#!/usr/bin/env bash # The interview asks, the stages consume, and nothing survives the run. # # Three properties matter enough to pin: # # 1. Every question maps to a stage that reads its answer. A prompt whose # answer nothing consumes is a control that lies -- the same defect this # repository refused to ship on the SSH Keys page -- and it is an easy one # to introduce, because asking is cheap and wiring up is not. # 2. Every answer a stage reads is one the interview asks. The reverse gap is # quieter and worse: the stage silently takes its fallback forever. # 3. The answers file is deleted on every exit path. It carries an email # address, and it is transient by design -- there is deliberately no # remembered state between runs. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" interview="$repo_dir/setup/scripts/interview" install_script="$repo_dir/install" findings=() note() { findings+=("$1"); } # ── 1 & 2. Questions and consumers agree ───────────────────────────────────── asked="$(grep -oE '^record [A-Z_]+' "$interview" | awk '{print $2}' | sort -u)" # Stages read answers as ${PANAMA_FOO:-default}; install re-exports them. consumed="$(grep -rhoE '\$\{PANAMA_[A-Z_]+' "$repo_dir"/setup/scripts/* "$install_script" 2>/dev/null \ | sed 's/^\${//' | sort -u)" # Not answers: paths the installer sets up for itself. INFRASTRUCTURE='^(PANAMA_PATH|PANAMA_ANSWERS|PANAMA_BASH|PANAMA_DOT|PANAMA_OLD|PANAMA_APPLICATION_DIR|PANAMA_ICON_DIR|PANAMA_UNIT_DIR|PANAMA_CURSOR_DIR|PANAMA_WALLPAPER_DIR)$' while read -r key; do [[ -n "$key" ]] || continue grep -qx "$key" <<<"$consumed" \ || note "the interview asks for $key, but no stage ever reads it" done <<<"$asked" while read -r key; do [[ -n "$key" ]] || continue [[ "$key" =~ $INFRASTRUCTURE ]] && continue grep -qx "$key" <<<"$asked" \ || note "a stage reads $key, but the interview never asks for it" done <<<"$consumed" # ── 3. Nothing is left behind ──────────────────────────────────────────────── grep -q 'trap cleanup EXIT INT TERM' "$install_script" \ || note 'install does not arm a cleanup trap on EXIT INT TERM' grep -q 'rm -f "$PANAMA_ANSWERS"' "$install_script" \ || note 'the cleanup trap does not delete the answers file' grep -qE 'mktemp' "$install_script" \ || note 'install does not create the answers file with mktemp' # Declining must stop the run rather than count as one failed stage among five. grep -qE 'if ! "\$PANAMA_PATH/setup/scripts/interview"; then' "$install_script" \ || note 'install does not treat a declined interview as fatal' # ── 4. A real run, with gum stubbed ────────────────────────────────────────── # # The interview is built on gum, which needs a terminal. Standing in a stub on # PATH exercises the actual script -- its ordering, its quoting, and the file it # writes -- rather than asserting things about its source text. stub_dir="$(mktemp -d)" answers_file="$(mktemp)" trap 'rm -rf "$stub_dir" "$answers_file"' EXIT cat >"$stub_dir/gum" <<'STUB' #!/usr/bin/env bash case "$1" in input) printf '%s\n' "$GUM_STUB_INPUT" ;; confirm) [[ "$GUM_STUB_CONFIRM" == yes ]] ;; style) shift; printf '%s\n' "${@: -1}" ;; *) exit 0 ;; esac STUB chmod +x "$stub_dir/gum" # A value containing a space and a quote, to prove %q survives being sourced. GUM_STUB_INPUT="O'Brien Test" GUM_STUB_CONFIRM=yes \ PANAMA_ANSWERS="$answers_file" PATH="$stub_dir:$PATH" \ bash "$interview" >/dev/null 2>&1 interview_status=$? (( interview_status == 0 )) || note "the interview exited $interview_status on a run that answered everything" # Sourcing it back must reproduce the value exactly, not a mangled fragment. ( # shellcheck source=/dev/null source "$answers_file" [[ "${PANAMA_GIT_NAME:-}" == "O'Brien Test" ]] ) || note 'an answer containing a quote and a space does not survive being sourced' # Declining at the confirmation must fail, so install stops. GUM_STUB_INPUT="x" GUM_STUB_CONFIRM=no \ PANAMA_ANSWERS="$answers_file" PATH="$stub_dir:$PATH" \ bash "$interview" >/dev/null 2>&1 \ && note 'declining the final confirmation still exits zero, so install would proceed' # Refusing to invent an answers path keeps the file where the caller can delete it. PATH="$stub_dir:$PATH" bash "$interview" >/dev/null 2>&1 \ && note 'the interview runs without PANAMA_ANSWERS instead of refusing' # ── Report ─────────────────────────────────────────────────────────────────── if (( ${#findings[@]} > 0 )); then mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u) printf 'interview contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'interview contract: PASS\n'