diff --git a/claude/code/skills/core/SKILL.md b/claude/code/skills/core/SKILL.md index 966d7e9..e8e50fa 100644 --- a/claude/code/skills/core/SKILL.md +++ b/claude/code/skills/core/SKILL.md @@ -13,6 +13,7 @@ The `core` command provides a unified interface for Go/PHP development and multi | Task | Command | |------|---------| +| Smart tests | `core test` | | Go tests | `core go test` | | Go coverage | `core go cov` | | Go format | `core go fmt --fix` | @@ -58,3 +59,15 @@ Multiple repos? | `git status` per repo | `core dev health` | Run `core --help` or `core --help` for full options. + +## Smart Test Runner: `core test` + +The `core test` command provides an intelligent way to run only the tests relevant to your recent changes. + +- **`core test`**: Automatically detects changed files since the last commit and runs only the corresponding tests. +- **`core test --all`**: Runs the entire test suite for the project. +- **`core test --filter `**: Runs a specific test by name. +- **`core test --coverage`**: Generates a test coverage report. +- **`core test `**: Runs tests for a specific file or directory. + +The runner automatically detects whether the project is Go or PHP and executes the appropriate testing tool. If it cannot map changed files to test files, it will fall back to running the full test suite. diff --git a/claude/code/skills/core/test.sh b/claude/code/skills/core/test.sh new file mode 100755 index 0000000..f37ea11 --- /dev/null +++ b/claude/code/skills/core/test.sh @@ -0,0 +1,167 @@ +#!/bin/bash +set -e # Exit on error + +# --- Configuration --- +# Set to 1 to enable debug messages +DEBUG=0 +debug() { + if [ "$DEBUG" -eq 1 ]; then + echo "DEBUG: $@" >&2 + fi +} + +# --- Argument Parsing --- +COVERAGE=false +FILTER="" +ALL=false +# All other arguments are treated as files/directories to test +PATHS=() + +while (( "$#" )); do + case "$1" in + --coverage) + COVERAGE=true + shift + ;; + --filter) + if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then + FILTER="$2" + shift 2 + else + echo "Error: Argument for --filter is missing" >&2 + exit 1 + fi + ;; + --all) + ALL=true + shift + ;; + -*) + echo "Error: Unknown option $1" >&2 + exit 1 + ;; + *) # preserve positional arguments as paths + PATHS+=("$1") + shift + ;; + esac +done + +debug "Coverage: $COVERAGE" +debug "Filter: $FILTER" +debug "All: $ALL" +debug "Paths: ${PATHS[*]}" + +# --- Project Detection --- +is_php() { [ -f "composer.json" ]; } +is_go() { [ -f "go.mod" ]; } + +# --- Test Execution Functions --- +run_php_tests() { + local args=("$@") + local cmd=("core" "php" "test") + if [ "$COVERAGE" = true ]; then + cmd+=("--coverage") + fi + if [ -n "$FILTER" ]; then + cmd+=("--filter" "$FILTER") + fi + if [ ${#args[@]} -gt 0 ]; then + cmd+=("${args[@]}") + fi + echo "Running: ${cmd[*]}" + "${cmd[@]}" +} + +run_go_tests() { + local args=("$@") + local cmd_base=("core" "go") + local cmd_action="test" + + if [ "$COVERAGE" = true ]; then + cmd_action="cov" + fi + + local cmd=("$cmd_base" "$cmd_action") + + if [ -n "$FILTER" ]; then + cmd+=("--run" "$FILTER") + fi + + if [ ${#args[@]} -gt 0 ]; then + # For Go, convert file paths to package paths (./path/to/pkg) + local pkgs=() + for p in "${args[@]}"; do + pkg=$(dirname "$p") + # Add ./ prefix if not present and avoid duplicates + if [[ ! " ${pkgs[@]} " =~ " ./$pkg " ]]; then + pkgs+=("./$pkg") + fi + done + cmd+=("${pkgs[@]}") + else + # If no paths specified, run for all packages + cmd+=("./...") + fi + + echo "Running: ${cmd[*]}" + "${cmd[@]}" +} + +# --- Main Logic --- + +# If specific paths are provided, use them +if [ ${#PATHS[@]} -gt 0 ]; then + echo "Running tests for specified paths..." + if is_php; then run_php_tests "${PATHS[@]}"; + elif is_go; then run_go_tests "${PATHS[@]}"; + else echo "No PHP or Go project detected." >&2; exit 1; fi + exit 0 +fi + +# If --all or --filter is used without paths, run against the whole project +if [ "$ALL" = true ] || [ -n "$FILTER" ]; then + echo "Running all tests (--all or --filter specified)..." + if is_php; then run_php_tests; + elif is_go; then run_go_tests; + else echo "No PHP or Go project detected." >&2; exit 1; fi + exit 0 +fi + +# --- Smart Detection (default behavior) --- +echo "No specific options provided. Detecting changes since last commit..." +changed_files=$(git diff --name-only HEAD~1 HEAD) + +if [ -z "$changed_files" ]; then + echo "No changed files detected. Running all tests." + if is_php; then run_php_tests; + elif is_go; then run_go_tests; + else echo "No PHP or Go project detected." >&2; exit 1; fi + exit 0 +fi + +echo -e "Detected changed files:\n$changed_files" +test_files=() + +for file in $changed_files; do + if is_php && [[ "$file" == src/*.php ]]; then + test_file=$(echo "$file" | sed 's|^src/|tests/|' | sed 's/\.php/Test.php/') + [ -f "$test_file" ] && test_files+=("$test_file") + elif is_go && [[ "$file" == *.go ]] && [[ "$file" != *_test.go ]]; then + test_file="${file%.go}_test.go" + [ -f "$test_file" ] && test_files+=("$test_file") + fi +done + +if [ ${#test_files[@]} -eq 0 ]; then + echo "Could not map changed files to any existing tests. Running all tests as a fallback." + if is_php; then run_php_tests; + elif is_go; then run_go_tests; + else echo "No PHP or Go project detected." >&2; exit 1; fi +else + echo "Running tests for the following files:" + printf " - %s\n" "${test_files[@]}" + if is_php; then run_php_tests "${test_files[@]}"; + elif is_go; then run_go_tests "${test_files[@]}"; + else echo "No PHP or Go project detected." >&2; exit 1; fi +fi