#!/usr/bin/env bash # Run only the Bats tests affected by the staged paths. # # Usage: # scripts/test-changed.sh # inspect the staged commit # scripts/test-changed.sh --list # print selected test files # scripts/test-changed.sh --paths FILE ... # # --paths is intended for tests and local experimentation. The normal mode is # deliberately staged-only so a pre-commit hook cannot accidentally validate # an unrelated dirty working-tree file. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" LIST_ONLY=0 USE_PATHS=0 declare -a CHANGED=() die() { printf 'ERROR: %s\n' "$*" >&2 exit 2 } while [ "$#" -gt 0 ]; do case "$1" in --list) LIST_ONLY=1 shift ;; --paths) USE_PATHS=1 shift while [ "$#" -gt 0 ] && [[ "$1" != --* ]]; do CHANGED+=("$1") shift done ;; --) shift CHANGED+=("$@") break ;; *) die "unknown option '$1' (use --list or --paths FILE ...)" ;; esac done cd "$REPO_ROOT" if [ "$USE_PATHS" -eq 0 ]; then while IFS= read -r -d '' path; do CHANGED+=("$path") done < <(git diff --cached --name-only -z --diff-filter=ACMRTUXB) fi [ "${#CHANGED[@]}" -gt 0 ] || { printf 'No staged paths; no focused tests to run.\n' exit 0 } declare -a TESTS=() add_test() { local test="$1" local existing MAPPED=1 for existing in "${TESTS[@]:-}"; do [ "$existing" != "$test" ] || return 0 done TESTS+=("$test") } # The path->test mapping has two layers: # 1. cross-cutting seams that no single service owns (compose validity, # env consistency, the deployer itself, docs, syntax); # 2. the service inventory (shared/services.yml via service-map.py # "seams"): every service the path selects on ANY host contributes its # declared tests. Service seams are the services' own — the former # shotgun fallbacks (every dankclaw quadlet unit ran the open-webui, # pihole, and searxng seams) are retired; the inventory names each # service's real seams, and adding a service is a data edit. for path in "${CHANGED[@]}"; do MAPPED=0 case "$path" in tests/*.bats) add_test "$path" continue ;; tests/helpers.bash|tests/run.sh) die "$path is shared test infrastructure; run ./tests/run.sh explicitly" ;; tests/e2e/*) add_test tests/e2e_scaffold.bats continue ;; .githooks/*) add_test tests/changed_tests.bats continue ;; # Archive material and host-level facts: a path under any # .AGENTTRASH/ directory is retired-by-policy (the planner plans # nothing from it), udev rules are applied by manual sudo install, # reports are frozen artifacts, and ignore-lists change nothing at # runtime. They select no focused test; without this case they would # trip the mapping gate below. .AGENTTRASH/*|*/.AGENTTRASH/*|dankclaw/udev/*|report/*|.gitignore|.env.example) continue ;; esac # Cross-cutting seams. case "$path" in dankserver/.env.example|dankclaw/.env.example) add_test tests/env_consistency.bats ;; dankserver/Caddyfile|dankclaw/Caddyfile) add_test tests/caddyfile.bats add_test tests/deployment_scoping.bats ;; scripts/deploy.sh|scripts/deploy-remote.sh|scripts/deploy-plan.py|scripts/deploy-*-quadlet.sh) add_test tests/deployment_flow.bats add_test tests/deployment_scoping.bats add_test tests/scripts_syntax.bats ;; scripts/service-map.py|shared/services.yml) add_test tests/service_inventory.bats add_test tests/deployment_scoping.bats add_test tests/scripts_syntax.bats ;; scripts/lib/*.sh) add_test tests/llm_gates.bats add_test tests/scripts_syntax.bats ;; serverAppConfigs-docs/*|README.md|AGENTS.md) add_test tests/docs_completeness.bats add_test tests/deployment_scoping.bats ;; scripts/mount-entropy.sh) add_test tests/mount_entropy.bats add_test tests/scripts_syntax.bats ;; old/scripts/*) add_test tests/scripts_syntax.bats ;; scripts/*) add_test tests/scripts_syntax.bats ;; esac # The inventory's declared seams for every service this path selects. while IFS= read -r seam; do [ -n "$seam" ] && add_test "$seam" done < <(python3 "$REPO_ROOT/scripts/service-map.py" seams -- "$path" 2>/dev/null || true) # Mapping gate: a path that selected no test at all — no service seam, # no cross-cutting rule — is an unmapped area of the repo; refuse to # commit it silently (the old catch-all `die`). if [ "$MAPPED" -eq 0 ]; then die "no focused-test mapping for '$path'; map it in the inventory (shared/services.yml) before committing" fi done # Mapped-but-untestable paths (udev rules, trash moves) legitimately produce an # empty selection; that is a pass, not a bats usage error. [ "${#TESTS[@]}" -gt 0 ] || exit 0 if [ "$LIST_ONLY" -eq 1 ]; then printf '%s\n' "${TESTS[@]}" exit 0 fi command -v bats >/dev/null 2>&1 || die 'bats is not installed' printf 'Running focused tests for %d changed path(s):\n' "${#CHANGED[@]}" printf ' %s\n' "${TESTS[@]}" bats --print-output-on-failure "${TESTS[@]}"