v1.4.0: Major Skills Expansion - 75 Total Skills
This commit is contained in:
165
skills/superpowers/tests/opencode/run-tests.sh
Normal file
165
skills/superpowers/tests/opencode/run-tests.sh
Normal file
@@ -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=$((skipped + 1))
|
||||
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=$((passed + 1))
|
||||
else
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
echo ""
|
||||
echo " [FAIL] $test (${duration}s)"
|
||||
failed=$((failed + 1))
|
||||
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=$((passed + 1))
|
||||
else
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
echo " [FAIL] (${duration}s)"
|
||||
echo ""
|
||||
echo " Output:"
|
||||
echo "$output" | sed 's/^/ /'
|
||||
failed=$((failed + 1))
|
||||
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
|
||||
73
skills/superpowers/tests/opencode/setup.sh
Normal file
73
skills/superpowers/tests/opencode/setup.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/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"
|
||||
export OPENCODE_CONFIG_DIR="$TEST_HOME/.config/opencode"
|
||||
|
||||
# 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/plugins"
|
||||
cp "$REPO_ROOT/.opencode/plugins/superpowers.js" "$HOME/.config/opencode/superpowers/.opencode/plugins/"
|
||||
|
||||
# Register plugin via symlink
|
||||
mkdir -p "$HOME/.config/opencode/plugins"
|
||||
ln -sf "$HOME/.config/opencode/superpowers/.opencode/plugins/superpowers.js" \
|
||||
"$HOME/.config/opencode/plugins/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/plugins/superpowers.js"
|
||||
echo "Plugin registered at: $HOME/.config/opencode/plugins/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
skills/superpowers/tests/opencode/test-plugin-loading.sh
Normal file
81
skills/superpowers/tests/opencode/test-plugin-loading.sh
Normal file
@@ -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/plugins/superpowers.js" ]; then
|
||||
echo " [PASS] Plugin symlink exists"
|
||||
else
|
||||
echo " [FAIL] Plugin symlink not found at $HOME/.config/opencode/plugins/superpowers.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify symlink target exists
|
||||
if [ -f "$(readlink -f "$HOME/.config/opencode/plugins/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/plugins/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
skills/superpowers/tests/opencode/test-priority.sh
Normal file
198
skills/superpowers/tests/opencode/test-priority.sh
Normal file
@@ -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 ==="
|
||||
440
skills/superpowers/tests/opencode/test-skills-core.sh
Normal file
440
skills/superpowers/tests/opencode/test-skills-core.sh
Normal file
@@ -0,0 +1,440 @@
|
||||
#!/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 ==="
|
||||
104
skills/superpowers/tests/opencode/test-tools.sh
Normal file
104
skills/superpowers/tests/opencode/test-tools.sh
Normal file
@@ -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 ==="
|
||||
Reference in New Issue
Block a user