Files
takana/scripts/upgrade-e2e-test.sh
T
Sergio 24bcf1783c takana etapa 4: los 10 crates de librería y el CLI pasan a takana-*
hammer-{core,build,bootstrap,overlay,journal,mirror,upgrade,agent,recover,cli}
→ takana-*, con sus deps de workspace, sus identificadores en el fuente y las
referencias -p de los scripts.

VERIFICADO que no mueve nada del corpus: `takana hash recipes/zlib.toml`
devuelve b3:dc363f26… , idéntico a antes del renombre. Los nombres de crate no
entran en hash_inputs, pero eso se comprueba, no se supone. 600 tests en verde.

DOS BINARIOS SE CONGELAN, y no por prolijidad:

- `hammerd` — paquete Y binario. Es componente de Stage 1 de la distro (musl,
  busybox, hammerd, arje-zero), lo supervisa arje-zero en el sistema arrancado,
  `PRESEED=hammerd` lo nombra en selfhost-verify y sus bytes anclan el baseline
  of_tree. El nombre del crate va en los símbolos ⇒ renombrarlo mueve los bytes.

- `hammer-recover` — el PAQUETE se renombra a takana-recover, el BINARIO no.
  hammer-live-install.sh lo copia a /usr/sbin/hammer-recover en sistemas ya
  instalados y hornea un hook de arranque que lo invoca por ese nombre:
  renombrarlo rompe máquinas instaladas, no el repo.

Consecuencia que hay que anotar igual: al renombrar hammer-core, los bytes de
hammerd cambian de todos modos porque linkea contra un crate con otro nombre.
El baseline of_tree del selfhost hay que rehacerlo — es efecto de la etapa 4,
no de un cambio de hammerd.

Las referencias en comentarios de recetas y docs (rutas hammer-core/src/…)
quedan para la etapa 5: son texto, no mueven hash.
2026-09-09 18:46:41 +00:00

129 lines
5.4 KiB
Bash
Executable File

#!/bin/sh
# E2E de la Etapa E4 (upgrades atómicos con rollback) contra el binario `hammer` REAL.
#
# Monta un store sintético con dos árboles "producto" (v1, v2), un root FHS vivo, y ejercita:
# apply v1 → apply v2 → rollback → rollback, verificando el estado del root en cada paso.
# Todo en host, sin VM: valida el cableado del CLI + la semántica generación/rollback de hammer-upgrade.
#
# Uso: scripts/upgrade-e2e-test.sh
set -eu
ROOT_DIR="$(CDPATH= cd "$(dirname "$0")/.." && pwd)"
WORK="$(mktemp -d)"
trap 'chmod -R u+w "$WORK" 2>/dev/null || true; rm -rf "$WORK"' EXIT
STORE="$WORK/store"
ROOT="$WORK/root"
STATE="$WORK/state"
JOURNAL="$WORK/journal"
mkdir -p "$STORE" "$ROOT/usr/bin" "$ROOT/etc"
echo "==> compilando hammer"
cargo build -q -p takana-cli --manifest-path "$ROOT_DIR/Cargo.toml"
HAMMER="$ROOT_DIR/target/debug/hammer"
up() { "$HAMMER" --store "$STORE" upgrade --root "$ROOT" --state-root "$STATE" --journal "$JOURNAL" "$@"; }
# --- estado pristino del root (lo que el rollback final debe restaurar) ---
printf 'PRISTINE-ls' > "$ROOT/usr/bin/ls"
printf 'motd original\n' > "$ROOT/etc/motd"
# --- sella dos árboles producto sintéticos en el store (vía `hammer build`? no: usamos un mini-store
# a mano replicando la forma <64hex>-<name>; el of_tree lo computa hammer al aplicar) ---
seal_tree() {
# $1=hex64 $2=name $3=builder-fn
dir="$STORE/$1-$2"
rm -rf "$dir"; mkdir -p "$dir"
"$3" "$dir"
chmod -R a-w "$dir" # el store sella read-only; replicamos para fidelidad
}
hex() { printf '%s' "$1"; i=${#1}; while [ "$i" -lt 64 ]; do printf '0'; i=$((i+1)); done; }
build_v1() {
mkdir -p "$1/usr/bin" "$1/bin"
printf 'LS-v1' > "$1/usr/bin/ls"
printf 'OLDtool'> "$1/usr/bin/oldtool"
ln -s busybox "$1/bin/sh"
}
build_v2() {
mkdir -p "$1/usr/bin" "$1/bin"
printf 'LS-v2' > "$1/usr/bin/ls"
printf 'NEWtool'> "$1/usr/bin/newtool" # oldtool desaparece
ln -s busybox "$1/bin/sh"
}
seal_tree "$(hex aa11)" product-rootfs build_v1
seal_tree "$(hex bb22)" product-rootfs build_v2
V1="$(hex aa11)-product-rootfs"
V2="$(hex bb22)-product-rootfs"
assert() { # $1=path $2=expected-content $3=label
got="$(cat "$1" 2>/dev/null || echo '<ausente>')"
if [ "$got" != "$2" ]; then echo "FALLO [$3]: $1 = '$got', esperaba '$2'" >&2; exit 1; fi
echo " ok [$3] $1 = $2"
}
assert_absent() { [ ! -e "$1" ] || { echo "FALLO [$2]: $1 debería estar ausente" >&2; exit 1; }; echo " ok [$2] $1 ausente"; }
echo "==> apply v1"
up apply "$V1"
assert "$ROOT/usr/bin/ls" "LS-v1" "v1"
assert "$ROOT/usr/bin/oldtool" "OLDtool" "v1"
[ -L "$ROOT/bin/sh" ] || { echo "FALLO: bin/sh no es symlink" >&2; exit 1; }
echo "==> apply v2 (pisa ls, añade newtool, RETIRA oldtool)"
up apply "$V2"
assert "$ROOT/usr/bin/ls" "LS-v2" "v2"
assert "$ROOT/usr/bin/newtool" "NEWtool" "v2"
assert_absent "$ROOT/usr/bin/oldtool" "v2"
echo "==> status"
up status
echo "==> rollback (v2 → v1: ls vuelve, newtool se borra, oldtool se restaura)"
up rollback
assert "$ROOT/usr/bin/ls" "LS-v1" "rb→v1"
assert "$ROOT/usr/bin/oldtool" "OLDtool" "rb→v1"
assert_absent "$ROOT/usr/bin/newtool" "rb→v1"
echo "==> rollback (v1 → pristino: el root vuelve EXACTO al estado pre-upgrades)"
up rollback
assert "$ROOT/usr/bin/ls" "PRISTINE-ls" "rb→pristino"
assert "$ROOT/etc/motd" "$(printf 'motd original\n')" "rb→pristino"
assert_absent "$ROOT/usr/bin/oldtool" "rb→pristino"
echo "==> idempotencia: re-apply v1 sobre pristino vuelve a generar, status coherente"
up apply "$V1" >/dev/null
up apply "$V1" # mismo árbol ⇒ no-op
echo "==> diario: registró cambios"
LINES="$(wc -l < "$JOURNAL/mutations.jsonl")"
[ "$LINES" -gt 0 ] || { echo "FALLO: diario vacío" >&2; exit 1; }
echo " ok: $LINES eventos en el diario"
echo "==> prune: tras los dos rollback completos las gens 1 y 2 quedaron huérfanas (current=3)"
GENS_ANTES="$(ls "$STATE/generations" | wc -l)"
up prune
GENS_DESPUES="$(ls "$STATE/generations" | wc -l)"
[ "$GENS_ANTES" -gt "$GENS_DESPUES" ] || { echo "FALLO: prune no borró huérfanas ($GENS_ANTES$GENS_DESPUES)" >&2; exit 1; }
echo " ok: prune $GENS_ANTES$GENS_DESPUES generación(es)"
# el árbol vivo (gen 3 = v1) sigue intacto tras el prune.
assert "$ROOT/usr/bin/ls" "LS-v1" "post-prune"
echo "==> recover: simulamos un apply interrumpido plantando un pending.json y lo recuperamos"
# El recover real se prueba a fondo en los tests unitarios (corte a media proyección). Acá ejercitamos
# el CABLEADO del CLI contra artefactos reales: plantamos el manifiesto de la gen viva como intento
# pendiente; status debe avisar y recover (roll-forward, idempotente) completarlo y limpiarlo.
CUR=$(cat "$STATE/current")
cp "$STATE/generations/$CUR/manifest.json" "$STATE/pending.json"
up status | grep -q "apply interrumpido pendiente" || { echo "FALLO: status no avisó del intento" >&2; exit 1; }
echo " ok: status avisó del intento pendiente"
up recover | grep -q "COMPLETADO" || { echo "FALLO: recover no completó" >&2; exit 1; }
[ ! -e "$STATE/pending.json" ] || { echo "FALLO: recover no limpió el intento" >&2; exit 1; }
assert "$ROOT/usr/bin/ls" "LS-v1" "post-recover"
echo " ok: recover completó el intento y lo limpió"
# segundo recover = no-op.
up recover | grep -q "nada que recuperar" || { echo "FALLO: recover no es no-op sin intento" >&2; exit 1; }
echo " ok: recover sin intento es no-op"
echo
echo "✓✓ E4 upgrade E2E VERDE: apply/rollback atómicos + restauración exacta + idempotencia + diario + prune + recover"