#!/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 for existing in "${TESTS[@]:-}"; do [ "$existing" != "$test" ] || return 0 done TESTS+=("$test") } for path in "${CHANGED[@]}"; do case "$path" in tests/*.bats) add_test "$path" ;; tests/helpers.bash|tests/run.sh) die "$path is shared test infrastructure; run ./tests/run.sh explicitly" ;; .githooks/*) add_test tests/changed_tests.bats ;; dankserver/compose.yaml|dankclaw/compose.yaml) add_test tests/compose_validity.bats add_test tests/env_consistency.bats add_test tests/deployment_scoping.bats ;; dankserver/Caddyfile|dankclaw/Caddyfile) add_test tests/caddyfile.bats add_test tests/deployment_scoping.bats ;; tests/e2e/*) add_test tests/e2e_scaffold.bats ;; dankserver/.env.example|dankclaw/.env.example) add_test tests/env_consistency.bats ;; shared/homepage/*) add_test tests/homepage_caddy.bats add_test tests/homepage_config.bats add_test tests/homepage_deploy.bats add_test tests/homepage_pages.bats add_test tests/homepage_checker.bats add_test tests/deployment_scoping.bats ;; shared/searxng/*) add_test tests/searxng_limiter.bats add_test tests/searxng_quadlet.bats add_test tests/deployment_scoping.bats ;; # The five observability containers left compose for Quadlet units on # 2026-09-11. Their seam pins boot persistence and the loopback-only # publish posture that keeps them off every host interface. This case # precedes the dankclaw/*/quadlet/*.container fallback below, which # would otherwise swallow it into unrelated seams. dankclaw/observability/*) add_test tests/observability_quadlet.bats add_test tests/caddyfile.bats add_test tests/deployment_scoping.bats add_test tests/docs_completeness.bats ;; # Caddy left compose.yaml on 2026-08-26 for a quadlet; caddyfile.bats # now asserts against the quadlet itself. Route every change under # dankclaw/caddy/ to both seams (this block must precede the # dankclaw/*.container glob, which would otherwise swallow the unit). dankclaw/caddy/*) add_test tests/caddy_quadlet.bats add_test tests/caddyfile.bats add_test tests/docs_completeness.bats add_test tests/deployment_scoping.bats ;; dankclaw/*.container|dankclaw/*.network) add_test tests/open_webui_quadlet.bats add_test tests/pihole.bats add_test tests/searxng_quadlet.bats add_test tests/deployment_scoping.bats ;; # Anubis is a quadlet + Caddyfile pair: the chain (public listener -> # anubis:8923 -> internal :8016 listener) spans both, so any change # under the service dir must re-run the caddyfile seam too. This case # precedes the dankclaw/*/quadlet/*.container fallback below, which # would otherwise swallow it into unrelated seams. dankclaw/anubis/*) add_test tests/anubis.bats add_test tests/caddyfile.bats add_test tests/homepage_caddy.bats add_test tests/deployment_scoping.bats ;; # frank-large is a rootless quadlet again since 2026-09-16 (the # halogen-flash-server restore). Its unit carries the fleet's GPU and # podman posture, so it gets its own seam rather than the generic # fallback below. dankclaw/frank-large/*) add_test tests/frank_large_unit.bats add_test tests/deployment_scoping.bats add_test tests/docs_completeness.bats ;; # Quadlets live under dankclaw//quadlet/ (reorganized out of the # top-level dankclaw/*.container globs above). Map the whole subtree so # changes to the pihole, open-webui, searxng, and gitea quadlets hit # their seam tests. The pihole-specific block must come first: shell # case matches the first pattern, and dankclaw/pihole/quadlet/*.container # also satisfies the dankclaw/*/quadlet/*.container fallback. dankclaw/pihole/*) add_test tests/pihole.bats add_test tests/docs_completeness.bats add_test tests/deployment_scoping.bats ;; dankclaw/*/quadlet/*.container|dankclaw/*/quadlet/*.network) add_test tests/open_webui_quadlet.bats add_test tests/pihole.bats add_test tests/searxng_quadlet.bats add_test tests/docs_completeness.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/mount-entropy.sh) add_test tests/mount_entropy.bats ;; # Archive material: a path under any .AGENTTRASH/ directory is # retired-by-policy (deploy-plan.py plans nothing from it), so it # selects no focused test. It still needs its own case: without one # the wrapper refuses retirement commits, whose trash moves show up # as additions, with "no focused-test mapping". .AGENTTRASH/*|*/.AGENTTRASH/*) ;; # frank-primary is the lucebox DS4.1 Quadlet (2026-09-26 cutover on # :7770): its entrypoint and healthcheck live under # dankserver/frank-primary/ and the container mounts them straight out # of the checkout, so any file here changes deployed state. The # radiance quadlet that held this endpoint before that date is archive # material in dankserver/.AGENTTRASH/ and maps to nothing. dankserver/frank-primary/*) add_test tests/frank_primary_unit.bats add_test tests/deployment_scoping.bats add_test tests/docs_completeness.bats ;; # frank-large's chat template lives in scripts/ (moved there on # 2026-09-26 when its old home was retired): a template edit changes # what the live endpoint renders, so it selects the large unit seam. scripts/qwen-sharp-v22.4.0.jinja) add_test tests/frank_large_unit.bats ;; # frank-small is llama.cpp on the 3090 as a ROOT unit: a native CUDA # binary, so no generated quadlet is involved. Its env, unit and serve # script all live in scripts/, which is why this case has to precede # the generic scripts/*.service and scripts/* fallbacks below. scripts/frank-small.*|scripts/serve-frank-small.sh) add_test tests/frank_small_unit.bats add_test tests/llm_units.bats add_test tests/deployment_scoping.bats add_test tests/scripts_syntax.bats ;; scripts/*.service) add_test tests/llm_units.bats add_test tests/scripts_syntax.bats add_test tests/deployment_scoping.bats ;; scripts/*) add_test tests/scripts_syntax.bats ;; serverAppConfigs-docs/*) add_test tests/docs_completeness.bats add_test tests/deployment_scoping.bats ;; dankclaw/hermes/*) add_test tests/docs_completeness.bats ;; README.md|AGENTS.md) # Project guidance changes can affect the documented deployment # contract; validate the docs vault and scoped deployment seams. add_test tests/docs_completeness.bats add_test tests/deployment_scoping.bats ;; dankclaw/comfyui/*) add_test tests/docs_completeness.bats add_test tests/deployment_scoping.bats ;; # loki and alloy images are built from this repo's Containerfiles; the # loki one carries the healthcheck command (see its comment), so both # seams read it. dankclaw/loki/*|dankclaw/alloy/*) add_test tests/observability_quadlet.bats add_test tests/caddyfile.bats add_test tests/deployment_scoping.bats ;; dankclaw/udev/*) # Host-level udev rules are applied by manual sudo install + udevadm # trigger, not dispatched as a service; nothing here is bats-testable. ;; .AGENTTRASH/*) # Trash moves are not testable; skip. ;; .gitignore|.env.example) # Ignore/ignore-list changes change nothing at runtime; not testable. ;; old/scripts/*|old/scripts/**) add_test tests/scripts_syntax.bats ;; # Cutover-era dankserver quadlet units (romm trio, jellyfin, calibre-web, # copyparty, plex, tautulli): the seam checks boot-persistence + # v1-generator contract; deploy-flow seams guard the deployer routing. dankserver/romm/*|dankserver/jellyfin/*|dankserver/calibre-web/*|dankserver/copyparty/*|dankserver/plex/*|dankserver/tautulli/*) add_test tests/dankserver_quadlet.bats add_test tests/deployment_flow.bats add_test tests/deployment_scoping.bats ;; # FrankBook-Pro units live inside the podman machine VM; the seam # pins boot persistence, the gluetun netns invariant, and the # plan-locked volume layout. Deployer changes route here too. frankbook-pro/*) add_test tests/frankbook_quadlet.bats add_test tests/deployment_flow.bats add_test tests/deployment_scoping.bats ;; # Seerr unit on dankclaw: boot persistence, loopback-only port # posture, and the deployer routing that installs it. dankclaw/seerr/*) add_test tests/seerr_quadlet.bats add_test tests/deployment_flow.bats add_test tests/deployment_scoping.bats ;; report/*) # Frozen audit reports are point-in-time artifacts; not testable. ;; *) die "no focused-test mapping for '$path'; add one before committing" ;; esac 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[@]}"