147 lines
4.1 KiB
Plaintext
147 lines
4.1 KiB
Plaintext
|
|
#!/usr/bin/env bats
|
||
|
|
# tests/lifecycle/bats/companion-survives-archipelago-restart.bats
|
||
|
|
#
|
||
|
|
# Quadlet promise: companion UIs (archy-bitcoin-ui, archy-lnd-ui,
|
||
|
|
# archy-electrs-ui) are managed by systemd, not archipelago. Restarting
|
||
|
|
# the archipelago user service must NOT take them down.
|
||
|
|
#
|
||
|
|
# This is the regression gate for the .228 incident in
|
||
|
|
# feedback_container_lifecycle_failure_modes.md (FM1: companions vanished
|
||
|
|
# from `podman ps -a` after archipelago crash-loop).
|
||
|
|
#
|
||
|
|
# Gated by ARCHY_ALLOW_DESTRUCTIVE=1 because it bounces archipelago.
|
||
|
|
|
||
|
|
companion_units=(
|
||
|
|
"archy-bitcoin-ui"
|
||
|
|
"archy-lnd-ui"
|
||
|
|
"archy-electrs-ui"
|
||
|
|
)
|
||
|
|
|
||
|
|
unit_dir="$HOME/.config/containers/systemd"
|
||
|
|
|
||
|
|
unit_file_present() {
|
||
|
|
local name="$1"
|
||
|
|
[[ -f "$unit_dir/$name.container" ]]
|
||
|
|
}
|
||
|
|
|
||
|
|
service_active() {
|
||
|
|
local name="$1"
|
||
|
|
systemctl --user is-active --quiet "$name.service"
|
||
|
|
}
|
||
|
|
|
||
|
|
container_running() {
|
||
|
|
local name="$1"
|
||
|
|
[[ "$(podman inspect --format '{{.State.Running}}' "$name" 2>/dev/null)" == "true" ]]
|
||
|
|
}
|
||
|
|
|
||
|
|
wait_service_active() {
|
||
|
|
local name="$1"
|
||
|
|
local timeout="${2:-60}"
|
||
|
|
local deadline=$(( $(date +%s) + timeout ))
|
||
|
|
while (( $(date +%s) < deadline )); do
|
||
|
|
if service_active "$name"; then
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
sleep 2
|
||
|
|
done
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
|
||
|
|
wait_archipelago_back() {
|
||
|
|
local timeout="${1:-60}"
|
||
|
|
local deadline=$(( $(date +%s) + timeout ))
|
||
|
|
while (( $(date +%s) < deadline )); do
|
||
|
|
if curl -fsS -o /dev/null "http://127.0.0.1:5678/health" 2>/dev/null; then
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
sleep 2
|
||
|
|
done
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
|
||
|
|
@test "destructive gate enabled" {
|
||
|
|
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
|
||
|
|
}
|
||
|
|
|
||
|
|
@test "every installed companion has a quadlet unit on disk" {
|
||
|
|
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
|
||
|
|
local present=0
|
||
|
|
for c in "${companion_units[@]}"; do
|
||
|
|
if container_running "$c"; then
|
||
|
|
run unit_file_present "$c"
|
||
|
|
[ "$status" -eq 0 ]
|
||
|
|
present=$(( present + 1 ))
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
(( present > 0 )) || skip "No companions installed on this node"
|
||
|
|
}
|
||
|
|
|
||
|
|
@test "every installed companion service is active before restart" {
|
||
|
|
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
|
||
|
|
for c in "${companion_units[@]}"; do
|
||
|
|
if container_running "$c"; then
|
||
|
|
run service_active "$c"
|
||
|
|
[ "$status" -eq 0 ]
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
@test "companions survive archipelago restart" {
|
||
|
|
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
|
||
|
|
|
||
|
|
# Snapshot: which companions were up before we touched anything.
|
||
|
|
local before=()
|
||
|
|
for c in "${companion_units[@]}"; do
|
||
|
|
if container_running "$c"; then
|
||
|
|
before+=("$c")
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
(( ${#before[@]} > 0 )) || skip "No companions installed on this node"
|
||
|
|
|
||
|
|
# Bounce archipelago. The user service is the production canonical name;
|
||
|
|
# fall back to the system service for older nodes.
|
||
|
|
if systemctl --user list-units --no-legend archipelago.service | grep -q archipelago; then
|
||
|
|
systemctl --user restart archipelago.service
|
||
|
|
else
|
||
|
|
sudo systemctl restart archipelago.service
|
||
|
|
fi
|
||
|
|
|
||
|
|
run wait_archipelago_back 60
|
||
|
|
[ "$status" -eq 0 ]
|
||
|
|
|
||
|
|
# Every companion that was up before must still be up + healthy after.
|
||
|
|
for c in "${before[@]}"; do
|
||
|
|
run service_active "$c"
|
||
|
|
[ "$status" -eq 0 ]
|
||
|
|
run container_running "$c"
|
||
|
|
[ "$status" -eq 0 ]
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
@test "deleted unit file is recreated within one reconcile tick" {
|
||
|
|
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
|
||
|
|
|
||
|
|
# Pick a companion that's currently running.
|
||
|
|
local target=""
|
||
|
|
for c in "${companion_units[@]}"; do
|
||
|
|
if container_running "$c"; then
|
||
|
|
target="$c"
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
[[ -n "$target" ]] || skip "No companions installed on this node"
|
||
|
|
|
||
|
|
# Delete the unit file behind systemd's back. The reconciler should
|
||
|
|
# notice and rewrite it within one 30s tick, then start the service.
|
||
|
|
rm -f "$unit_dir/$target.container"
|
||
|
|
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||
|
|
systemctl --user stop "$target.service" >/dev/null 2>&1 || true
|
||
|
|
|
||
|
|
# Allow up to two reconcile ticks (60s + grace).
|
||
|
|
run wait_service_active "$target" 90
|
||
|
|
[ "$status" -eq 0 ]
|
||
|
|
|
||
|
|
run unit_file_present "$target"
|
||
|
|
[ "$status" -eq 0 ]
|
||
|
|
}
|