#!/usr/bin/env bash # What rig has settled, written down as assertions. # # These are documentation that runs. Each check is ONE decision that has already # been made, with the reason above it — not coverage, and deliberately not an # exhaustive sweep of use cases. rig's own index says a rule without its reason # gets overridden the first time it is inconvenient; a rule nobody can restate # is worse. So the test says what was decided, and failing it should read as # "you are about to undo this" rather than "something broke". # # Scope, on purpose: # - no cluster, no docker, no network. It must be cheap enough to actually run. # - it asserts about RIG. `make check` asserts about the MACHINE and never # fails; this exits 1, the way `make standalone check` does. # - what actually deploys is not testable here. `tilt ci` stays a manual step. # # Usage: make selftest (or: bash ctrl/selftest.sh) set -uo pipefail # NOT -e: one failing check must not abort the rest cd "$(dirname "$0")" source ./lib/config.sh rc=0 passed=0 check() { # name, expected, actual if [ "$2" = "$3" ]; then printf ' ok %s\n' "$1" passed=$((passed + 1)) else printf ' FAIL %s\n expected: %s\n got: %s\n' "$1" "$2" "$3" rc=1 fi } note() { printf '\n%s\n' "$1"; } # Resolve one key the way every rig script does, in a clean shell so the # caller's exported value is the only thing in play. resolved() { bash -c 'source ./lib/config.sh; load_config >/dev/null 2>&1; printf "%s" "${!1}"' _ "$1" } note "the ports.sh active contract" # ports.sh active is read POSITIONALLY by two other files — the Makefile takes # $(word 2) and $(word 5), the Tiltfile takes _facts[0]..[6]. Insert a field in # the middle and nothing errors: Tilt simply guards on the wrong context or # binds the wrong port. The field count and order are the contract, so they are # pinned here rather than left to whoever edits ports.sh next. FACTS="$(bash ports.sh active)" check "active: exactly 7 fields" "7" "$(printf '%s' "$FACTS" | wc -w)" read -r F_CLUSTER F_CTX F_HTTP F_HTTPS F_TILT F_REG F_MANIFESTS <<< "$FACTS" check "active: field 2 is kind-" "kind-$F_CLUSTER" "$F_CTX" check "active: fields 3-6 are numeric" "yes" \ "$([[ "$F_HTTP$F_HTTPS$F_TILT$F_REG" =~ ^[0-9]+$ ]] && echo yes || echo no)" check "active: field 7 is a path" "yes" \ "$([ -n "$F_MANIFESTS" ] && [ "${F_MANIFESTS#-}" = "$F_MANIFESTS" ] && echo yes || echo no)" # derive answers a different question and must keep its own shape: it reports # what the directory name implies, ignoring ctrl/.env, so nothing should # configure itself from it. check "derive: still 4 fields, not 7" "4" "$(bash ports.sh derive | wc -w)" note "the caller's env beats the files" # lib/config.sh states one precedence rule: versions.env < env.d/ < # ctrl/.env < the caller's env. It is enforced by CONFIG_OVERRIDABLE, a # hand-maintained list — and a key missing from it loses to the file SILENTLY. # REGISTRY_PORT and MANIFESTS_DIR were both missing on 2026-09-13 and were found # by accident. # # So this loop is generated FROM the list: add a key to CONFIG_OVERRIDABLE and # this test starts asking about it without anyone remembering to come here. # Three keys name something that must exist and are validated at load, so they # get a real alternative rather than a sentinel. test_value() { case "$1" in PROFILE) echo "client" ;; # env.d/client.env exists K8S_VERSION) echo "v1_35" ;; # NODE_IMAGE_v1_35 is pinned KIND_CONFIG) echo "kind-config.client.yaml.tpl" ;; # the shape must exist *_PORT) echo "19999" ;; CLUSTER) echo "selftest-name" ;; MANIFESTS_DIR) echo "../elsewhere/overlays/dev" ;; ADDONS) echo "metallb" ;; *) echo "selftest-sentinel" ;; esac } for key in $CONFIG_OVERRIDABLE; do [ -n "$key" ] || continue want="$(test_value "$key")" if [ -z "$want" ]; then check "precedence: $key has a test value" "yes" "no — add one to test_value()" continue fi got="$(export "$key=$want"; resolved "$key")" check "precedence: caller's $key wins" "$want" "$got" done note "one derivation, not three" # The Makefile used to compute the cluster name itself and sed TILT_PORT out of # ctrl/.env — a second derivation of values lib/config.sh already owns, which # could disagree with it after `make ports persist`. It now reads ports.sh # active. Nothing structurally prevents the sed coming back, so the agreement is # asserted against the real `make -n` output rather than against the source. # --no-print-directory and a grep, not `tail -1`: run from `make selftest` this # is a RECURSIVE make, and the "Entering/Leaving directory" lines go to STDOUT. # tail -1 then reads "make[1]: Leaving directory ..." and both checks below fail # — but only when invoked through make, never when the script is run directly. # A test that passes one way and fails the other is worse than no test. MK="$(cd .. && make --no-print-directory -n tilt 2>/dev/null | grep -m1 'tilt ')" check "Makefile: --context comes from active" "$F_CTX" \ "$(printf '%s' "$MK" | sed -n 's/.*--context \([^ ]*\).*/\1/p')" check "Makefile: --port comes from active" "$F_TILT" \ "$(printf '%s' "$MK" | sed -n 's/.*--port \([^ ]*\).*/\1/p')" note "identity follows the folder, safely" # The cluster name is NOT the bare directory name: kind needs a DNS label, so # default_cluster_name lowercases it and replaces everything outside [a-z0-9-]. # Re-deriving that anywhere else is how a copy ends up guarding the wrong # context — which is exactly why the Tiltfile asks instead of computing. TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT mkdir -p "$TMP/My_Proj" cp -r . "$TMP/My_Proj/ctrl" # A pinned CLUSTER in .env would be an override, not a derivation, and this # check is about the derivation. sed -i '/^CLUSTER=/d' "$TMP/My_Proj/ctrl/.env" 2>/dev/null COPY="$(cd "$TMP/My_Proj/ctrl" && bash ports.sh active)" check "a dir named My_Proj derives a DNS label" "my-proj" "$(awk '{print $1}' <<< "$COPY")" check "and a context to match" "kind-my-proj" "$(awk '{print $2}' <<< "$COPY")" check "a renamed copy gets a DIFFERENT block" "different" \ "$([ "$(awk '{print $3}' <<< "$COPY")" != "$F_HTTP" ] && echo different || echo COLLIDES)" note "ports are stable across versions" # Not a change-detector. The block is derived, never stored, so if the # derivation shifts then every EXISTING environment's ports move underneath it — # a running cluster keeps its old ports while rig starts reporting new ones, and # `make ports` stops describing reality. Anchored to three known names. check "derive_port_base rig" "20310" "$(derive_port_base rig)" check "derive_port_base foo" "21690" "$(derive_port_base foo)" check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)" note "rig stays standalone" # rig sits inside a host project's tree but must be copyable straight out of it: # no imports, no paths, no assumption the host is there. This grep is the whole # test of that claim, and until now it lived only in prose and in whoever # remembered to run it. # # The pattern is assembled from fragments so this file does not match ITSELF. # Writing it literally would fail forever; excluding this file instead would put # a blind spot in the one check that guards the boundary. HOST_PAT="$(printf '%s' 'sole' 'print' '|\b' 'sp' 'r\b')" check "no host-project references" "0" \ "$(cd .. && grep -rIl -iE "$HOST_PAT" . --exclude-dir=def 2>/dev/null | wc -l)" note "the Tiltfile hardcodes nothing" # Every other Tiltfile on this machine writes its slug in five or six times by # hand, so a copied project deploys into the original's cluster until someone # edits all of them. rig's asks ports.sh. A literal kind- here would mean # that has been undone. check "no literal kind-" "0" "$(grep -cE "['\"]kind-[a-z0-9]" Tiltfile)" check "guards on the variable" "1" "$(grep -c 'allow_k8s_contexts(CTX)' Tiltfile)" check "asks ports.sh for facts" "1" "$(grep -c "local('bash ports.sh active'" Tiltfile)" note "standalone kits are generated, current, and call only real verbs" # The kits under standalone// are rig flattened into single files, one # per profile. A kit left behind by a change to rig is exactly the drift they # replaced — rigmini.sh once said 2 GB per node long after rig measured 800 MB — # so a stale kit fails here rather than waiting to be noticed on another machine. check "every kit matches what rig generates now" "yes" \ "$(bash standalone.sh check >/dev/null 2>&1 && echo yes || echo "no — run make standalone")" # Each kit's Makefile exists so nothing wrapping these scripts has to GUESS how to # call them. A generated Makefile once did guess: `rigmini.sh on`, not a verb, # and a bare `rigdeps.sh` for "check and report", which installs. So every # target's default verb must be one its script's own dispatch accepts — read # from that dispatch, not from a list here that could drift from it. verbs_of() { sed -n '/^case "\$cmd" in/,/^esac/p' "$1" | grep -oE '^ [a-z]+\)' | tr -d ' )' } kits=0 for mk in ../standalone/*/Makefile; do [ -f "$mk" ] || continue kit=$(dirname "$mk"); kits=$((kits + 1)) for target in $(grep -oE '^[a-z][a-z-]*:' "$mk" | tr -d ':' | grep -vx help); do line="$(make --no-print-directory -s -n -f "$mk" "$target" 2>/dev/null | head -1)" script=$(basename "$(printf '%s' "$line" | awk '{print $2}')") verb=$(printf '%s' "$line" | awk '{print $NF}') check "$(basename "$kit"): make $target -> $script $verb, a verb it accepts" "yes" \ "$(verbs_of "$kit/$script" | grep -qx "$verb" && echo yes || echo "no: '$verb'")" done check "$(basename "$kit"): no \`mini\` target, which already means minimal footprint" "0" \ "$(grep -cE '^mini:' "$mk")" done check "there is a kit for every profile" "$(config_profiles | wc -l)" "$kits" note "optional — needs tilt and this rig's cluster" # Parsing the Tiltfile for real is the only way to know it still evaluates, but # Tilt snapshots a kubectl context before parsing, so it cannot run without a # cluster. Skipped rather than failed when there is none, the same way docgen # skips its graphgen section. if ! command -v tilt >/dev/null; then printf ' skip tilt is not installed\n' elif ! kubectl config get-contexts -o name 2>/dev/null | grep -qx "$F_CTX"; then printf " skip no %s context — run 'make cluster up' to include this\n" "$F_CTX" else out="$(tilt alpha tiltfile-result --context "$F_CTX" 2>&1)" check "Tiltfile evaluates" "yes" \ "$(printf '%s' "$out" | grep -q '"Manifests"' && echo yes || echo "no: $(printf '%s' "$out" | tail -1)")" fi printf '\n' if [ "$rc" -eq 0 ]; then printf '%d checks passed — rig still does what it says\n' "$passed" else printf 'FAILED — a decision above has drifted; read the comment next to it\n' >&2 fi exit "$rc"