ソースを参照

test: add automated test suite for opencode plugin

Create comprehensive tests in tests/opencode/ that verify the plugin
works correctly in an isolated test environment:

- setup.sh: Creates temp HOME and installs plugin with proper symlinks
- test-plugin-loading.sh: Verifies plugin structure and JavaScript syntax
- test-skills-core.sh: Unit tests for skills-core.js library functions
- test-tools.sh: Integration tests for use_skill/find_skills (requires OpenCode)
- test-priority.sh: Tests project > personal > superpowers priority
- run-tests.sh: Main test runner with options for integration tests

Run basic tests: ./tests/opencode/run-tests.sh
Run all tests: ./tests/opencode/run-tests.sh --integration
Jesse Vincent 9 ヶ月 前
コミット
f3d6c331a1

+ 165 - 0
tests/opencode/run-tests.sh

@@ -0,0 +1,165 @@
+#!/usr/bin/env bash
+# Main test runner for OpenCode plugin test suite
+# Runs all tests and reports results
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+cd "$SCRIPT_DIR"
+
+echo "========================================"
+echo " OpenCode Plugin Test Suite"
+echo "========================================"
+echo ""
+echo "Repository: $(cd ../.. && pwd)"
+echo "Test time: $(date)"
+echo ""
+
+# Parse command line arguments
+RUN_INTEGRATION=false
+VERBOSE=false
+SPECIFIC_TEST=""
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --integration|-i)
+            RUN_INTEGRATION=true
+            shift
+            ;;
+        --verbose|-v)
+            VERBOSE=true
+            shift
+            ;;
+        --test|-t)
+            SPECIFIC_TEST="$2"
+            shift 2
+            ;;
+        --help|-h)
+            echo "Usage: $0 [options]"
+            echo ""
+            echo "Options:"
+            echo "  --integration, -i  Run integration tests (requires OpenCode)"
+            echo "  --verbose, -v      Show verbose output"
+            echo "  --test, -t NAME    Run only the specified test"
+            echo "  --help, -h         Show this help"
+            echo ""
+            echo "Tests:"
+            echo "  test-plugin-loading.sh  Verify plugin installation and structure"
+            echo "  test-skills-core.sh     Test skills-core.js library functions"
+            echo "  test-tools.sh           Test use_skill and find_skills tools (integration)"
+            echo "  test-priority.sh        Test skill priority resolution (integration)"
+            exit 0
+            ;;
+        *)
+            echo "Unknown option: $1"
+            echo "Use --help for usage information"
+            exit 1
+            ;;
+    esac
+done
+
+# List of tests to run (no external dependencies)
+tests=(
+    "test-plugin-loading.sh"
+    "test-skills-core.sh"
+)
+
+# Integration tests (require OpenCode)
+integration_tests=(
+    "test-tools.sh"
+    "test-priority.sh"
+)
+
+# Add integration tests if requested
+if [ "$RUN_INTEGRATION" = true ]; then
+    tests+=("${integration_tests[@]}")
+fi
+
+# Filter to specific test if requested
+if [ -n "$SPECIFIC_TEST" ]; then
+    tests=("$SPECIFIC_TEST")
+fi
+
+# Track results
+passed=0
+failed=0
+skipped=0
+
+# Run each test
+for test in "${tests[@]}"; do
+    echo "----------------------------------------"
+    echo "Running: $test"
+    echo "----------------------------------------"
+
+    test_path="$SCRIPT_DIR/$test"
+
+    if [ ! -f "$test_path" ]; then
+        echo "  [SKIP] Test file not found: $test"
+        ((skipped++))
+        continue
+    fi
+
+    if [ ! -x "$test_path" ]; then
+        echo "  Making $test executable..."
+        chmod +x "$test_path"
+    fi
+
+    start_time=$(date +%s)
+
+    if [ "$VERBOSE" = true ]; then
+        if bash "$test_path"; then
+            end_time=$(date +%s)
+            duration=$((end_time - start_time))
+            echo ""
+            echo "  [PASS] $test (${duration}s)"
+            ((passed++))
+        else
+            end_time=$(date +%s)
+            duration=$((end_time - start_time))
+            echo ""
+            echo "  [FAIL] $test (${duration}s)"
+            ((failed++))
+        fi
+    else
+        # Capture output for non-verbose mode
+        if output=$(bash "$test_path" 2>&1); then
+            end_time=$(date +%s)
+            duration=$((end_time - start_time))
+            echo "  [PASS] (${duration}s)"
+            ((passed++))
+        else
+            end_time=$(date +%s)
+            duration=$((end_time - start_time))
+            echo "  [FAIL] (${duration}s)"
+            echo ""
+            echo "  Output:"
+            echo "$output" | sed 's/^/    /'
+            ((failed++))
+        fi
+    fi
+
+    echo ""
+done
+
+# Print summary
+echo "========================================"
+echo " Test Results Summary"
+echo "========================================"
+echo ""
+echo "  Passed:  $passed"
+echo "  Failed:  $failed"
+echo "  Skipped: $skipped"
+echo ""
+
+if [ "$RUN_INTEGRATION" = false ] && [ ${#integration_tests[@]} -gt 0 ]; then
+    echo "Note: Integration tests were not run."
+    echo "Use --integration flag to run tests that require OpenCode."
+    echo ""
+fi
+
+if [ $failed -gt 0 ]; then
+    echo "STATUS: FAILED"
+    exit 1
+else
+    echo "STATUS: PASSED"
+    exit 0
+fi

+ 72 - 0
tests/opencode/setup.sh

@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# Setup script for OpenCode plugin tests
+# Creates an isolated test environment with proper plugin installation
+set -euo pipefail
+
+# Get the repository root (two levels up from tests/opencode/)
+REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+
+# Create temp home directory for isolation
+export TEST_HOME=$(mktemp -d)
+export HOME="$TEST_HOME"
+export XDG_CONFIG_HOME="$TEST_HOME/.config"
+
+# Install plugin to test location
+mkdir -p "$HOME/.config/opencode/superpowers"
+cp -r "$REPO_ROOT/lib" "$HOME/.config/opencode/superpowers/"
+cp -r "$REPO_ROOT/skills" "$HOME/.config/opencode/superpowers/"
+
+# Copy plugin directory
+mkdir -p "$HOME/.config/opencode/superpowers/.opencode/plugin"
+cp "$REPO_ROOT/.opencode/plugin/superpowers.js" "$HOME/.config/opencode/superpowers/.opencode/plugin/"
+
+# Register plugin via symlink
+mkdir -p "$HOME/.config/opencode/plugin"
+ln -sf "$HOME/.config/opencode/superpowers/.opencode/plugin/superpowers.js" \
+       "$HOME/.config/opencode/plugin/superpowers.js"
+
+# Create test skills in different locations for testing
+
+# Personal test skill
+mkdir -p "$HOME/.config/opencode/skills/personal-test"
+cat > "$HOME/.config/opencode/skills/personal-test/SKILL.md" <<'EOF'
+---
+name: personal-test
+description: Test personal skill for verification
+---
+# Personal Test Skill
+
+This is a personal skill used for testing.
+
+PERSONAL_SKILL_MARKER_12345
+EOF
+
+# Create a project directory for project-level skill tests
+mkdir -p "$TEST_HOME/test-project/.opencode/skills/project-test"
+cat > "$TEST_HOME/test-project/.opencode/skills/project-test/SKILL.md" <<'EOF'
+---
+name: project-test
+description: Test project skill for verification
+---
+# Project Test Skill
+
+This is a project skill used for testing.
+
+PROJECT_SKILL_MARKER_67890
+EOF
+
+echo "Setup complete: $TEST_HOME"
+echo "Plugin installed to: $HOME/.config/opencode/superpowers/.opencode/plugin/superpowers.js"
+echo "Plugin registered at: $HOME/.config/opencode/plugin/superpowers.js"
+echo "Test project at: $TEST_HOME/test-project"
+
+# Helper function for cleanup (call from tests or trap)
+cleanup_test_env() {
+    if [ -n "${TEST_HOME:-}" ] && [ -d "$TEST_HOME" ]; then
+        rm -rf "$TEST_HOME"
+    fi
+}
+
+# Export for use in tests
+export -f cleanup_test_env
+export REPO_ROOT

+ 81 - 0
tests/opencode/test-plugin-loading.sh

@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+# Test: Plugin Loading
+# Verifies that the superpowers plugin loads correctly in OpenCode
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+echo "=== Test: Plugin Loading ==="
+
+# Source setup to create isolated environment
+source "$SCRIPT_DIR/setup.sh"
+
+# Trap to cleanup on exit
+trap cleanup_test_env EXIT
+
+# Test 1: Verify plugin file exists and is registered
+echo "Test 1: Checking plugin registration..."
+if [ -L "$HOME/.config/opencode/plugin/superpowers.js" ]; then
+    echo "  [PASS] Plugin symlink exists"
+else
+    echo "  [FAIL] Plugin symlink not found at $HOME/.config/opencode/plugin/superpowers.js"
+    exit 1
+fi
+
+# Verify symlink target exists
+if [ -f "$(readlink -f "$HOME/.config/opencode/plugin/superpowers.js")" ]; then
+    echo "  [PASS] Plugin symlink target exists"
+else
+    echo "  [FAIL] Plugin symlink target does not exist"
+    exit 1
+fi
+
+# Test 2: Verify lib/skills-core.js is in place
+echo "Test 2: Checking skills-core.js..."
+if [ -f "$HOME/.config/opencode/superpowers/lib/skills-core.js" ]; then
+    echo "  [PASS] skills-core.js exists"
+else
+    echo "  [FAIL] skills-core.js not found"
+    exit 1
+fi
+
+# Test 3: Verify skills directory is populated
+echo "Test 3: Checking skills directory..."
+skill_count=$(find "$HOME/.config/opencode/superpowers/skills" -name "SKILL.md" | wc -l)
+if [ "$skill_count" -gt 0 ]; then
+    echo "  [PASS] Found $skill_count skills installed"
+else
+    echo "  [FAIL] No skills found in installed location"
+    exit 1
+fi
+
+# Test 4: Check using-superpowers skill exists (critical for bootstrap)
+echo "Test 4: Checking using-superpowers skill (required for bootstrap)..."
+if [ -f "$HOME/.config/opencode/superpowers/skills/using-superpowers/SKILL.md" ]; then
+    echo "  [PASS] using-superpowers skill exists"
+else
+    echo "  [FAIL] using-superpowers skill not found (required for bootstrap)"
+    exit 1
+fi
+
+# Test 5: Verify plugin JavaScript syntax (basic check)
+echo "Test 5: Checking plugin JavaScript syntax..."
+plugin_file="$HOME/.config/opencode/superpowers/.opencode/plugin/superpowers.js"
+if node --check "$plugin_file" 2>/dev/null; then
+    echo "  [PASS] Plugin JavaScript syntax is valid"
+else
+    echo "  [FAIL] Plugin has JavaScript syntax errors"
+    exit 1
+fi
+
+# Test 6: Verify personal test skill was created
+echo "Test 6: Checking test fixtures..."
+if [ -f "$HOME/.config/opencode/skills/personal-test/SKILL.md" ]; then
+    echo "  [PASS] Personal test skill fixture created"
+else
+    echo "  [FAIL] Personal test skill fixture not found"
+    exit 1
+fi
+
+echo ""
+echo "=== All plugin loading tests passed ==="

+ 198 - 0
tests/opencode/test-priority.sh

@@ -0,0 +1,198 @@
+#!/usr/bin/env bash
+# Test: Skill Priority Resolution
+# Verifies that skills are resolved with correct priority: project > personal > superpowers
+# NOTE: These tests require OpenCode to be installed and configured
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+echo "=== Test: Skill Priority Resolution ==="
+
+# Source setup to create isolated environment
+source "$SCRIPT_DIR/setup.sh"
+
+# Trap to cleanup on exit
+trap cleanup_test_env EXIT
+
+# Create same skill "priority-test" in all three locations with different markers
+echo "Setting up priority test fixtures..."
+
+# 1. Create in superpowers location (lowest priority)
+mkdir -p "$HOME/.config/opencode/superpowers/skills/priority-test"
+cat > "$HOME/.config/opencode/superpowers/skills/priority-test/SKILL.md" <<'EOF'
+---
+name: priority-test
+description: Superpowers version of priority test skill
+---
+# Priority Test Skill (Superpowers Version)
+
+This is the SUPERPOWERS version of the priority test skill.
+
+PRIORITY_MARKER_SUPERPOWERS_VERSION
+EOF
+
+# 2. Create in personal location (medium priority)
+mkdir -p "$HOME/.config/opencode/skills/priority-test"
+cat > "$HOME/.config/opencode/skills/priority-test/SKILL.md" <<'EOF'
+---
+name: priority-test
+description: Personal version of priority test skill
+---
+# Priority Test Skill (Personal Version)
+
+This is the PERSONAL version of the priority test skill.
+
+PRIORITY_MARKER_PERSONAL_VERSION
+EOF
+
+# 3. Create in project location (highest priority)
+mkdir -p "$TEST_HOME/test-project/.opencode/skills/priority-test"
+cat > "$TEST_HOME/test-project/.opencode/skills/priority-test/SKILL.md" <<'EOF'
+---
+name: priority-test
+description: Project version of priority test skill
+---
+# Priority Test Skill (Project Version)
+
+This is the PROJECT version of the priority test skill.
+
+PRIORITY_MARKER_PROJECT_VERSION
+EOF
+
+echo "  Created priority-test skill in all three locations"
+
+# Test 1: Verify fixture setup
+echo ""
+echo "Test 1: Verifying test fixtures..."
+
+if [ -f "$HOME/.config/opencode/superpowers/skills/priority-test/SKILL.md" ]; then
+    echo "  [PASS] Superpowers version exists"
+else
+    echo "  [FAIL] Superpowers version missing"
+    exit 1
+fi
+
+if [ -f "$HOME/.config/opencode/skills/priority-test/SKILL.md" ]; then
+    echo "  [PASS] Personal version exists"
+else
+    echo "  [FAIL] Personal version missing"
+    exit 1
+fi
+
+if [ -f "$TEST_HOME/test-project/.opencode/skills/priority-test/SKILL.md" ]; then
+    echo "  [PASS] Project version exists"
+else
+    echo "  [FAIL] Project version missing"
+    exit 1
+fi
+
+# Check if opencode is available for integration tests
+if ! command -v opencode &> /dev/null; then
+    echo ""
+    echo "  [SKIP] OpenCode not installed - skipping integration tests"
+    echo "  To run these tests, install OpenCode: https://opencode.ai"
+    echo ""
+    echo "=== Priority fixture tests passed (integration tests skipped) ==="
+    exit 0
+fi
+
+# Test 2: Test that personal overrides superpowers
+echo ""
+echo "Test 2: Testing personal > superpowers priority..."
+echo "  Running from outside project directory..."
+
+# Run from HOME (not in project) - should get personal version
+cd "$HOME"
+output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load the priority-test skill. Show me the exact content including any PRIORITY_MARKER text." 2>&1) || {
+    exit_code=$?
+    if [ $exit_code -eq 124 ]; then
+        echo "  [FAIL] OpenCode timed out after 60s"
+        exit 1
+    fi
+}
+
+if echo "$output" | grep -qi "PRIORITY_MARKER_PERSONAL_VERSION"; then
+    echo "  [PASS] Personal version loaded (overrides superpowers)"
+elif echo "$output" | grep -qi "PRIORITY_MARKER_SUPERPOWERS_VERSION"; then
+    echo "  [FAIL] Superpowers version loaded instead of personal"
+    exit 1
+else
+    echo "  [WARN] Could not verify priority marker in output"
+    echo "  Output snippet:"
+    echo "$output" | grep -i "priority\|personal\|superpowers" | head -10
+fi
+
+# Test 3: Test that project overrides both personal and superpowers
+echo ""
+echo "Test 3: Testing project > personal > superpowers priority..."
+echo "  Running from project directory..."
+
+# Run from project directory - should get project version
+cd "$TEST_HOME/test-project"
+output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load the priority-test skill. Show me the exact content including any PRIORITY_MARKER text." 2>&1) || {
+    exit_code=$?
+    if [ $exit_code -eq 124 ]; then
+        echo "  [FAIL] OpenCode timed out after 60s"
+        exit 1
+    fi
+}
+
+if echo "$output" | grep -qi "PRIORITY_MARKER_PROJECT_VERSION"; then
+    echo "  [PASS] Project version loaded (highest priority)"
+elif echo "$output" | grep -qi "PRIORITY_MARKER_PERSONAL_VERSION"; then
+    echo "  [FAIL] Personal version loaded instead of project"
+    exit 1
+elif echo "$output" | grep -qi "PRIORITY_MARKER_SUPERPOWERS_VERSION"; then
+    echo "  [FAIL] Superpowers version loaded instead of project"
+    exit 1
+else
+    echo "  [WARN] Could not verify priority marker in output"
+    echo "  Output snippet:"
+    echo "$output" | grep -i "priority\|project\|personal" | head -10
+fi
+
+# Test 4: Test explicit superpowers: prefix bypasses priority
+echo ""
+echo "Test 4: Testing superpowers: prefix forces superpowers version..."
+
+cd "$TEST_HOME/test-project"
+output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load superpowers:priority-test specifically. Show me the exact content including any PRIORITY_MARKER text." 2>&1) || {
+    exit_code=$?
+    if [ $exit_code -eq 124 ]; then
+        echo "  [FAIL] OpenCode timed out after 60s"
+        exit 1
+    fi
+}
+
+if echo "$output" | grep -qi "PRIORITY_MARKER_SUPERPOWERS_VERSION"; then
+    echo "  [PASS] superpowers: prefix correctly forces superpowers version"
+elif echo "$output" | grep -qi "PRIORITY_MARKER_PROJECT_VERSION\|PRIORITY_MARKER_PERSONAL_VERSION"; then
+    echo "  [FAIL] superpowers: prefix did not force superpowers version"
+    exit 1
+else
+    echo "  [WARN] Could not verify priority marker in output"
+fi
+
+# Test 5: Test explicit project: prefix
+echo ""
+echo "Test 5: Testing project: prefix forces project version..."
+
+cd "$HOME"  # Run from outside project but with project: prefix
+output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load project:priority-test specifically. Show me the exact content." 2>&1) || {
+    exit_code=$?
+    if [ $exit_code -eq 124 ]; then
+        echo "  [FAIL] OpenCode timed out after 60s"
+        exit 1
+    fi
+}
+
+# Note: This may fail since we're not in the project directory
+# The project: prefix only works when in a project context
+if echo "$output" | grep -qi "not found\|error"; then
+    echo "  [PASS] project: prefix correctly fails when not in project context"
+else
+    echo "  [INFO] project: prefix behavior outside project context may vary"
+fi
+
+echo ""
+echo "=== All priority tests passed ==="

+ 376 - 0
tests/opencode/test-skills-core.sh

@@ -0,0 +1,376 @@
+#!/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
+result=$(node --input-type=module <<'NODESCRIPT'
+import { extractFrontmatter } from '$HOME/.config/opencode/superpowers/lib/skills-core.js';
+const result = extractFrontmatter(process.env.TEST_HOME + '/test-skill/SKILL.md');
+console.log(JSON.stringify(result));
+NODESCRIPT
+) 2>&1 || true
+
+# Try alternative approach if module import fails
+if ! echo "$result" | grep -q "test-skill"; then
+    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)
+fi
+
+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
+
+echo ""
+echo "=== All skills-core library tests passed ==="

+ 104 - 0
tests/opencode/test-tools.sh

@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+# Test: Tools Functionality
+# Verifies that use_skill and find_skills tools work correctly
+# NOTE: These tests require OpenCode to be installed and configured
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+echo "=== Test: Tools Functionality ==="
+
+# Source setup to create isolated environment
+source "$SCRIPT_DIR/setup.sh"
+
+# Trap to cleanup on exit
+trap cleanup_test_env EXIT
+
+# Check if opencode is available
+if ! command -v opencode &> /dev/null; then
+    echo "  [SKIP] OpenCode not installed - skipping integration tests"
+    echo "  To run these tests, install OpenCode: https://opencode.ai"
+    exit 0
+fi
+
+# Test 1: Test find_skills tool via direct invocation
+echo "Test 1: Testing find_skills tool..."
+echo "  Running opencode with find_skills request..."
+
+# Use timeout to prevent hanging, capture both stdout and stderr
+output=$(timeout 60s opencode run --print-logs "Use the find_skills tool to list available skills. Just call the tool and show me the raw output." 2>&1) || {
+    exit_code=$?
+    if [ $exit_code -eq 124 ]; then
+        echo "  [FAIL] OpenCode timed out after 60s"
+        exit 1
+    fi
+    echo "  [WARN] OpenCode returned non-zero exit code: $exit_code"
+}
+
+# Check for expected patterns in output
+if echo "$output" | grep -qi "superpowers:brainstorming\|superpowers:using-superpowers\|Available skills"; then
+    echo "  [PASS] find_skills tool discovered superpowers skills"
+else
+    echo "  [FAIL] find_skills did not return expected skills"
+    echo "  Output was:"
+    echo "$output" | head -50
+    exit 1
+fi
+
+# Check if personal test skill was found
+if echo "$output" | grep -qi "personal-test"; then
+    echo "  [PASS] find_skills found personal test skill"
+else
+    echo "  [WARN] personal test skill not found in output (may be ok if tool returned subset)"
+fi
+
+# Test 2: Test use_skill tool
+echo ""
+echo "Test 2: Testing use_skill tool..."
+echo "  Running opencode with use_skill request..."
+
+output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load the personal-test skill and show me what you get." 2>&1) || {
+    exit_code=$?
+    if [ $exit_code -eq 124 ]; then
+        echo "  [FAIL] OpenCode timed out after 60s"
+        exit 1
+    fi
+    echo "  [WARN] OpenCode returned non-zero exit code: $exit_code"
+}
+
+# Check for the skill marker we embedded
+if echo "$output" | grep -qi "PERSONAL_SKILL_MARKER_12345\|Personal Test Skill\|Launching skill"; then
+    echo "  [PASS] use_skill loaded personal-test skill content"
+else
+    echo "  [FAIL] use_skill did not load personal-test skill correctly"
+    echo "  Output was:"
+    echo "$output" | head -50
+    exit 1
+fi
+
+# Test 3: Test use_skill with superpowers: prefix
+echo ""
+echo "Test 3: Testing use_skill with superpowers: prefix..."
+echo "  Running opencode with superpowers:brainstorming skill..."
+
+output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load superpowers:brainstorming and tell me the first few lines of what you received." 2>&1) || {
+    exit_code=$?
+    if [ $exit_code -eq 124 ]; then
+        echo "  [FAIL] OpenCode timed out after 60s"
+        exit 1
+    fi
+    echo "  [WARN] OpenCode returned non-zero exit code: $exit_code"
+}
+
+# Check for expected content from brainstorming skill
+if echo "$output" | grep -qi "brainstorming\|Launching skill\|skill.*loaded"; then
+    echo "  [PASS] use_skill loaded superpowers:brainstorming skill"
+else
+    echo "  [FAIL] use_skill did not load superpowers:brainstorming correctly"
+    echo "  Output was:"
+    echo "$output" | head -50
+    exit 1
+fi
+
+echo ""
+echo "=== All tools tests passed ==="