Test: Exercise updates through the real command

This commit is contained in:
Gabriel Brown
2026-08-26 22:46:46 -04:00
parent 6a7bdcb835
commit 803362ebad
+139 -62
View File
@@ -37,7 +37,8 @@ tmp="$(mktemp -d -t panama-update-contract.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT trap 'rm -rf "$tmp"' EXIT
STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
link-vicinae-scripts setup-identity install-hardware) link-vicinae-scripts setup-server link-server setup-identity
install-hardware)
# A PANAMA_PATH that looks enough like the real one for install to run, and # A PANAMA_PATH that looks enough like the real one for install to run, and
# records what it was asked to do instead of doing it. # records what it was asked to do instead of doing it.
@@ -108,14 +109,13 @@ EOF
# Run the fixture installer and echo what ran, one stage per line. # Run the fixture installer and echo what ran, one stage per line.
run_install() { run_install() {
local root="$1"; shift local root="$1"; shift
local status=0
: >"$root/ran" : >"$root/ran"
PATH="$root/shim:$PATH" \ PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
PANAMA_PATH="$root" \ XDG_STATE_HOME="$root/state" bash "$root/install" "$@" \
PANAMA_RAN="$root/ran" \ >"$root/out" 2>&1 || status=$?
XDG_STATE_HOME="$root/state" \
bash "$root/install" "$@" >"$root/out" 2>&1
printf '%s' "$?" >"$root/rc"
cat "$root/ran" cat "$root/ran"
return "$status"
} }
# ── 1. The interview never runs on an upgrade ──────────────────────────────── # ── 1. The interview never runs on an upgrade ────────────────────────────────
@@ -146,12 +146,28 @@ done
# A stage added to STAGES without a decision about which path owns it shows up # A stage added to STAGES without a decision about which path owns it shows up
# here, because this list is written down twice on purpose. # here, because this list is written down twice on purpose.
mapfile -t declared < <(python3 - "$installer" <<'PY' declared=()
import re, sys if declared_output="$(python3 - "$installer" <<'PY'
line = next(l for l in open(sys.argv[1], encoding="utf-8") if l.startswith("STAGES=")) import re
print("\n".join(re.findall(r"[\w-]+", line)[1:])) import sys
found = False
for line in open(sys.argv[1], encoding="utf-8"):
match = re.match(r'^\s*STAGES=\((.*)\)\s*$', line)
if match and "$" not in match.group(1):
found = True
print("\n".join(re.findall(r"[\w-]+", match.group(1))))
if not found:
print("install has no STAGES assignment", file=sys.stderr)
sys.exit(1)
PY PY
) )"; then
while IFS= read -r stage; do
[[ -n "$stage" ]] && declared+=("$stage")
done <<<"$declared_output"
else
note 'could not read the install STAGES assignment'
fi
for stage in "${declared[@]}"; do for stage in "${declared[@]}"; do
printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \ printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \
|| note "install declares a stage this contract has never heard of: $stage" || note "install declares a stage this contract has never heard of: $stage"
@@ -177,7 +193,10 @@ grep -qx 'install-packages' <<<"$ran_changed" \
# A failing stage must not record the hash, or the failure is hidden forever. # A failing stage must not record the hash, or the failure is hidden forever.
build_fixture "$tmp/c" 1 build_fixture "$tmp/c" 1
run_install "$tmp/c" --upgrade >/dev/null install_status=0
run_install "$tmp/c" --upgrade >/dev/null || install_status=$?
[[ "$install_status" -eq 1 ]] \
|| note "install --upgrade returned $install_status instead of the failing stage status"
if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then
note 'install-packages failed but its hash was recorded, so it will never be retried' note 'install-packages failed but its hash was recorded, so it will never be retried'
fi fi
@@ -191,62 +210,120 @@ grep -qx 'install-packages' <<<"$ran_full" \
# ── 4. A conflicted pop never leaves markers in a live config ──────────────── # ── 4. A conflicted pop never leaves markers in a live config ────────────────
# #
# Two halves. The first checks that git still behaves the way the design # The fixture also covers a clean fast-forward, installer status propagation,
# depends on; the second checks that panama acts on it. Neither is worth much # and the boundary between update and sync before forcing the conflict below.
# without the other.
conflict="$tmp/conflict" # Each fixture has the same three repositories as a real update: a bare remote,
mkdir -p "$conflict" # a clone that publishes upstream changes, and the machine clone being updated.
( build_cli_fixture() (
set -e set -e
cd "$conflict" local root="$1"
git init -q up && cd up rm -rf "$root"
git config user.email contract@panama && git config user.name contract mkdir -p "$root"
printf 'one\n' >f; git add -A; git commit -qm one git init -q --bare "$root/origin.git"
cd "$conflict"; git clone -q up work; cd work git clone -q "$root/origin.git" "$root/upstream" 2>/dev/null
git config user.email contract@panama && git config user.name contract git -C "$root/upstream" config user.email contract@panama
cd "$conflict/up"; printf 'upstream\n' >f; git commit -qam two git -C "$root/upstream" config user.name contract
cd "$conflict/work"; printf 'local\n' >f
git stash push --include-untracked -m contract >/dev/null
git pull -q --ff-only
git stash pop >/dev/null 2>&1 && exit 3 # a conflict was the point
git reset -q --hard HEAD
[[ -n "$(git stash list)" ]] || exit 4 # the stash must survive
grep -q '<<<<<<<' f && exit 5 # and no markers may remain
exit 0
) >/dev/null 2>&1
case $? in
0) ;;
3) note 'the conflict fixture did not conflict, so this check proves nothing' ;;
4) note 'git no longer keeps the stash after a conflicted pop; panama update would lose work' ;;
5) note 'git reset --hard left conflict markers behind' ;;
*) note 'the stash conflict fixture could not be built' ;;
esac
# panama update must act on that: reset the tree rather than leave the markers. mkdir -p "$root/upstream/bin"
body="$(sed -n '/^cmd_update()/,/^}/p' "$panama")" cp "$panama" "$root/upstream/bin/panama"
if [[ -z "$body" ]]; then cat >"$root/upstream/install" <<'EOF'
note 'bin/panama has no cmd_update to check' #!/usr/bin/env bash
printf '%s\n' "$*" >>"${PANAMA_UPDATE_FIXTURE_LOG:?}"
exit "${PANAMA_UPDATE_INSTALL_RC:-0}"
EOF
chmod +x "$root/upstream/bin/panama" "$root/upstream/install"
printf 'one\n' >"$root/upstream/f"
git -C "$root/upstream" add -A
git -C "$root/upstream" commit -qm initial
git -C "$root/upstream" push -qu origin HEAD
git clone -q "$root/origin.git" "$root/machine"
git -C "$root/machine" config user.email contract@panama
git -C "$root/machine" config user.name contract
)
advance_upstream() (
set -e
local root="$1" file="$2" contents="$3"
printf '%s\n' "$contents" >"$root/upstream/$file"
git -C "$root/upstream" add "$file"
git -C "$root/upstream" commit -qm "update $file"
git -C "$root/upstream" push -q
)
clean="$tmp/clean-update"
if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
machine_before="$(git -C "$clean/machine" rev-parse HEAD)"
upstream_after="$(git -C "$clean/upstream" rev-parse HEAD)"
[[ "$machine_before" != "$upstream_after" ]] \
|| note 'the clean update fixture started current, so it cannot prove a fast-forward'
: >"$clean/install.log"
update_status=0
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
"$clean/machine/bin/panama" update >"$clean/update.out" 2>&1 \
|| update_status=$?
[[ "$update_status" -eq 0 ]] \
|| note "panama update failed on a clean clone with status $update_status"
[[ "$(git -C "$clean/machine" rev-parse HEAD)" == "$upstream_after" ]] \
|| note 'panama update did not fast-forward the clean machine clone'
grep -qx -- '--upgrade' "$clean/install.log" \
|| note 'panama update did not invoke the installer with --upgrade'
: >"$clean/install.log"
update_status=0
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" PANAMA_UPDATE_INSTALL_RC=23 \
"$clean/machine/bin/panama" update >"$clean/failing-update.out" 2>&1 \
|| update_status=$?
[[ "$update_status" -eq 23 ]] \
|| note "panama update returned $update_status instead of installer status 23"
grep -qx -- '--upgrade' "$clean/install.log" \
|| note 'the failing update did not reach the fixture installer'
: >"$clean/install.log"
printf 'local sync\n' >"$clean/machine/synced"
sync_status=0
printf 'y\ncontract sync\n' \
| PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
"$clean/machine/bin/panama" sync >"$clean/sync.out" 2>&1 \
|| sync_status=$?
[[ "$sync_status" -eq 0 ]] \
|| note "panama sync failed in the local fixture with status $sync_status"
[[ ! -s "$clean/install.log" ]] \
|| note 'panama sync invoked the installer; sync and update are separate jobs'
else else
grep -q 'git stash push' <<<"$body" \ note 'the clean update fixture could not be built'
|| note 'cmd_update does not stash local changes, so a pull can fail on a dirty tree'
grep -q 'git reset --hard' <<<"$body" \
|| note 'cmd_update does not reset after a failed pop, so conflict markers reach ~/.config'
grep -q 'git pull --ff-only' <<<"$body" \
|| note 'cmd_update does not pull with --ff-only'
grep -q -- '--upgrade' <<<"$body" \
|| note 'cmd_update does not hand off to install --upgrade, so it would ask the interview'
fi fi
# The two verbs stay separate: sync must never run the installer. # A conflicted stash pop must not leave markers in the checkout. The failed pop
sync_body="$(sed -n '/^cmd_sync()/,/^}/p' "$panama")" # keeps the stash, so the local version remains recoverable after the reset.
if [[ -z "$sync_body" ]]; then conflict="$tmp/conflict-update"
note 'bin/panama has no cmd_sync, so the git workflow lost its home' if build_cli_fixture "$conflict"; then
printf 'local\n' >"$conflict/machine/f"
if advance_upstream "$conflict" f upstream; then
: >"$conflict/install.log"
conflict_status=0
PANAMA_UPDATE_FIXTURE_LOG="$conflict/install.log" \
"$conflict/machine/bin/panama" update >"$conflict/update.out" 2>&1 \
|| conflict_status=$?
[[ "$conflict_status" -eq 0 ]] \
|| note "panama update failed while recovering a stash conflict with status $conflict_status"
[[ "$(<"$conflict/machine/f")" == upstream ]] \
|| note 'panama update did not reset the conflicted file to the upstream version'
if git -C "$conflict/machine" grep -qE '^(<<<<<<<|=======|>>>>>>>)' -- .; then
note 'panama update left conflict markers in the machine checkout'
fi
[[ -n "$(git -C "$conflict/machine" stash list)" ]] \
|| note 'panama update dropped the stash after its conflicted pop'
recovered="$(git -C "$conflict/machine" show 'stash@{0}:f' 2>/dev/null)"
[[ "$recovered" == local ]] \
|| note 'the stash left by panama update does not contain the local version'
else else
grep -q 'install' <<<"$sync_body" \ note 'the conflict fixture could not publish its upstream edit'
&& note 'cmd_sync runs the installer; committing and updating are separate jobs' fi
grep -q 'git stash' <<<"$sync_body" \ else
&& note 'cmd_sync stashes, which the commit-before-pull order exists to avoid' note 'the conflict update fixture could not be built'
fi fi
if (( ${#findings[@]} > 0 )); then if (( ${#findings[@]} > 0 )); then