test(lifecycle): add btcpay + fedimint + mempool suites

Brings L1 (RPC API) + L3 (lifecycle survival) parity coverage to the
three multi-app stacks that were previously only touched by
required-stack.bats. Combined with bitcoin-knots / lnd / electrumx
already shipping, the six core apps now have dedicated bats files.

Each suite is shaped like the existing single-container suites
(bitcoin-knots / lnd / electrumx) and gates every assertion on the
backing container actually being present, so a node without the stack
installed gets clean skip messages instead of false fails.

* btcpay.bats — 9 tests, including stack-wide presence and a
  "supporting containers don't cascade-restart" guard
* fedimint.bats — 8 tests, single container
* mempool.bats — 9 tests, mixed legacy + orchestrator-managed stack;
  reuses the :8999 mempool-api probe from required-stack for parity

Total bats now: 88 (was 53 → +35).
TESTING.md matrix advances 23 → 50 of 110 cells.
UI URL coverage for these three apps already lives in
ui-coverage.bats, so this PR doesn't duplicate proxy-path probes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-05-01 16:55:31 -04:00
parent ec1dce93a9
commit 5074572373
4 changed files with 436 additions and 5 deletions

View File

@ -39,15 +39,15 @@ Legend: ● fully covered, ◐ partial, ○ missing
| bitcoin-core | ◐ shares with knots | ◐ | ○ | ◐ | ○ | ○ | ○ | ○ | ○ | ◐ regression-gate |
| lnd | ● | ● | ● (lncli) | ● (`/app/lnd/`) | ● | ● | ● | ● | ○ | ◐ regression-gate |
| electrumx | ● | ● | ● (TCP 50001) | ● (`/app/electrumx/`) | ● | ● | ● | ● | ○ | ◐ regression-gate |
| btcpay-server | ◐ via required-stack | ○ | ○ | ● probe-only | ○ | ○ | ○ | ○ | ○ | ○ |
| mempool | ◐ via required-stack | ○ | ◐ via required-stack | ● probe-only | ○ | ○ | ○ | ○ | ○ | ○ |
| fedimint | ◐ via required-stack | ○ | ○ | ● probe-only | ○ | ○ | ○ | ○ | ○ | ○ |
| btcpay-server | ● | ● | ◐ frontend-port | ● (`/app/btcpay/`) | ● | ● | ● | ● | ○ | ○ |
| mempool | ● | ● | ● (`/api/v1/backend-info`) | ● (`/app/mempool/`) | ● | ● | ● | ● | ○ | ○ |
| fedimint | ● | ● | ◐ container-only | ● (`/app/fedimint/`) | ● | ● | ● | ● | ○ | ○ |
| filebrowser | ○ | ○ | ○ | ● probe-only | ○ | ○ | ○ | ○ | ○ | ◐ via companions |
| archy-bitcoin-ui | ◐ via companions | ◐ | n/a | ● (port 8334) | ○ | ○ | ○ | n/a | ◐ via companions | ● |
| archy-lnd-ui | ◐ via companions | ◐ | n/a | ● (`/app/lnd/`) | ○ | ○ | ○ | n/a | ◐ via companions | ● |
| archy-electrs-ui | ◐ via companions | ◐ | n/a | ● (`/app/electrumx/`) | ○ | ○ | ○ | n/a | ◐ via companions | ● |
Done: 23 of 110 cells. Goal: 110/110 ● for the listed apps before
Done: 50 of 110 cells. Goal: 110/110 ● for the listed apps before
v1.7.52 tags.
### Layer-by-layer status
@ -55,7 +55,7 @@ v1.7.52 tags.
| Layer | Tests | Suites | Status |
|---|---:|---:|---|
| L0 unit | 615 | n/a | ● green |
| L1 RPC | 30 → growing | bitcoin-knots, lnd, electrumx, required-stack, package-update-smoke | ● for the 3 single-container core apps |
| L1 RPC | 70 | bitcoin-knots, lnd, electrumx, btcpay, mempool, fedimint, required-stack, package-update-smoke | ● for the 6 core apps |
| L2 UI | 9 | ui-coverage | ● for dashboard + 7 proxy paths + bitcoin-ui:8334 |
| L3 lifecycle survival | 8 | companion-survives-archipelago-restart, backend-survives-archipelago-restart, required-stack-destructive | ◐ companions ● ; backends ◐ regression-gate (will fail until Phase 3 Quadlet ships) |
| L4 browser journey | 0 | none | ○ not started |

View File

@ -0,0 +1,146 @@
#!/usr/bin/env bats
# tests/lifecycle/bats/btcpay.bats
#
# Lifecycle tests for the btcpay-server multi-container stack:
# - btcpay-server (the main app)
# - archy-btcpay-db (postgres)
# - archy-nbxplorer (Bitcoin watcher)
#
# Multi-container variant of bitcoin-knots.bats / lnd.bats / electrumx.bats.
# UI URL coverage is in ui-coverage.bats; this suite is L1 (RPC API) + L3
# (lifecycle survival).
#
# Pre-req: btcpay-server installed, bitcoin-knots running.
load '../lib/rpc.bash'
setup_file() {
: "${ARCHY_PASSWORD:?Set ARCHY_PASSWORD env var to the UI password}"
export ARCHY_FORCE_LOGIN=1
rpc_login
unset ARCHY_FORCE_LOGIN
}
teardown_file() {
rpc_logout_local
}
btcpay_components=(
"btcpay-server"
"archy-btcpay-db"
"archy-nbxplorer"
)
@test "container-list includes every btcpay-stack component" {
run rpc_result container-list
[ "$status" -eq 0 ]
for c in "${btcpay_components[@]}"; do
echo "$output" | jq -e --arg n "$c" '.[] | select(.name == $n)' >/dev/null \
|| skip "btcpay component $c not present (stack not installed)"
done
}
@test "container-list reports valid states for every btcpay component" {
run rpc_result container-list
[ "$status" -eq 0 ]
local present=0
for c in "${btcpay_components[@]}"; do
local state
state=$(echo "$output" | jq -r --arg n "$c" '.[] | select(.name == $n) | .state')
[[ -n "$state" ]] || continue
present=$((present + 1))
[[ "$state" =~ ^(running|stopped|exited|created|paused)$ ]] \
|| fail "invalid state for $c: $state"
done
(( present > 0 )) || skip "btcpay stack not installed"
}
@test "no orphan btcpay-related containers beyond the known set" {
local total known
total=$(podman ps -a --format '{{.Names}}' \
| grep -Ec '^(btcpay|archy-btcpay|archy-nbxplorer)' || true)
known=$(podman ps -a --format '{{.Names}}' \
| grep -Ec '^(btcpay-server|archy-btcpay-db|archy-nbxplorer)$' || true)
[ "$total" -eq "$known" ]
}
# ────────────────────────────────────────────────────────────────────
# Destructive tier
# ────────────────────────────────────────────────────────────────────
@test "package.stop transitions btcpay-server to stopped" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
podman inspect btcpay-server --format '{{.State.Status}}' >/dev/null 2>&1 \
|| skip "btcpay-server not installed"
run rpc_result package.stop '{"id":"btcpay-server"}'
[ "$status" -eq 0 ]
run wait_for_container_status btcpay-server stopped 60
[ "$status" -eq 0 ]
}
@test "package.start brings btcpay-server back to running" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
podman inspect btcpay-server --format '{{.State.Status}}' >/dev/null 2>&1 \
|| skip "btcpay-server not installed"
run rpc_result package.start '{"id":"btcpay-server"}'
[ "$status" -eq 0 ]
run wait_for_container_status btcpay-server running 180
[ "$status" -eq 0 ]
}
@test "package.restart leaves btcpay-server in running state" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
podman inspect btcpay-server --format '{{.State.Status}}' >/dev/null 2>&1 \
|| skip "btcpay-server not installed"
run rpc_result package.restart '{"id":"btcpay-server"}'
[ "$status" -eq 0 ]
run wait_for_container_status btcpay-server running 180
[ "$status" -eq 0 ]
}
@test "db + nbxplorer remain running across btcpay-server restart" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
for c in archy-btcpay-db archy-nbxplorer; do
podman inspect "$c" --format '{{.State.Status}}' >/dev/null 2>&1 \
|| skip "btcpay supporting container $c not installed"
done
for c in archy-btcpay-db archy-nbxplorer; do
local state
state=$(podman inspect --format '{{.State.Status}}' "$c" 2>/dev/null)
[[ "$state" == "running" ]] \
|| fail "supporting btcpay container $c is not running (state=$state) — package.restart cascaded into it"
done
}
@test "package.uninstall removes the whole btcpay stack" {
[[ "${ARCHY_ALLOW_CASCADE_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_CASCADE_DESTRUCTIVE not set"
podman inspect btcpay-server --format '{{.State.Status}}' >/dev/null 2>&1 \
|| skip "btcpay-server not installed"
run rpc_result package.uninstall '{"id":"btcpay-server","preserve_data":true}'
[ "$status" -eq 0 ]
for c in "${btcpay_components[@]}"; do
run wait_for_container_status "$c" absent 120
[ "$status" -eq 0 ] || fail "btcpay component $c not removed by uninstall"
done
}
@test "package.install restores the whole btcpay stack" {
[[ "${ARCHY_ALLOW_CASCADE_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_CASCADE_DESTRUCTIVE not set"
run rpc_result package.install '{"manifest_path":"btcpay-server/manifest.yaml"}'
[ "$status" -eq 0 ]
for c in "${btcpay_components[@]}"; do
run wait_for_container_status "$c" running 240
[ "$status" -eq 0 ] || fail "btcpay component $c never reached running after reinstall"
done
}

View File

@ -0,0 +1,113 @@
#!/usr/bin/env bats
# tests/lifecycle/bats/fedimint.bats
#
# Lifecycle tests for the fedimint package. The fedimint federation
# daemon runs as a single container; the gateway is its own package
# (fedimint-gateway). Mirrors the single-container pattern of
# lnd.bats / electrumx.bats for L1 (RPC API) + L3 (lifecycle survival).
# UI URL coverage is in ui-coverage.bats.
load '../lib/rpc.bash'
setup_file() {
: "${ARCHY_PASSWORD:?Set ARCHY_PASSWORD env var to the UI password}"
export ARCHY_FORCE_LOGIN=1
rpc_login
unset ARCHY_FORCE_LOGIN
}
teardown_file() {
rpc_logout_local
}
fedimint_skip_if_absent() {
podman inspect fedimint --format '{{.State.Status}}' >/dev/null 2>&1 \
|| skip "fedimint not installed"
}
@test "container-list includes fedimint" {
run rpc_result container-list
[ "$status" -eq 0 ]
echo "$output" | jq -e '.[] | select(.name == "fedimint")' >/dev/null \
|| skip "fedimint not installed"
}
@test "container-list reports a valid state for fedimint" {
fedimint_skip_if_absent
run rpc_result container-list
[ "$status" -eq 0 ]
local state
state=$(echo "$output" | jq -r '.[] | select(.name == "fedimint") | .state')
[[ "$state" =~ ^(running|stopped|exited|created|paused)$ ]]
}
@test "no orphan fedimint-related containers beyond the known set" {
local total known
total=$(podman ps -a --format '{{.Names}}' \
| grep -Ec '^(fedimint|fedimintd|fedimint-gateway)' || true)
known=$(podman ps -a --format '{{.Names}}' \
| grep -Ec '^(fedimint|fedimint-gateway)$' || true)
[ "$total" -eq "$known" ]
}
# ────────────────────────────────────────────────────────────────────
# Destructive tier
# ────────────────────────────────────────────────────────────────────
@test "package.stop transitions fedimint to stopped" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
fedimint_skip_if_absent
run rpc_result package.stop '{"id":"fedimint"}'
[ "$status" -eq 0 ]
run wait_for_container_status fedimint stopped 60
[ "$status" -eq 0 ]
}
@test "package.start brings fedimint back to running" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
fedimint_skip_if_absent
run rpc_result package.start '{"id":"fedimint"}'
[ "$status" -eq 0 ]
run wait_for_container_status fedimint running 180
[ "$status" -eq 0 ]
}
@test "package.restart leaves fedimint in running state" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
fedimint_skip_if_absent
run rpc_result package.restart '{"id":"fedimint"}'
[ "$status" -eq 0 ]
run wait_for_container_status fedimint running 180
[ "$status" -eq 0 ]
}
# ────────────────────────────────────────────────────────────────────
# Cascade-destructive tier
# ────────────────────────────────────────────────────────────────────
@test "package.uninstall removes fedimint" {
[[ "${ARCHY_ALLOW_CASCADE_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_CASCADE_DESTRUCTIVE not set"
fedimint_skip_if_absent
run rpc_result package.uninstall '{"id":"fedimint","preserve_data":true}'
[ "$status" -eq 0 ]
run wait_for_container_status fedimint absent 120
[ "$status" -eq 0 ]
}
@test "package.install fedimint returns to running" {
[[ "${ARCHY_ALLOW_CASCADE_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_CASCADE_DESTRUCTIVE not set"
run rpc_result package.install '{"manifest_path":"fedimint/manifest.yaml"}'
[ "$status" -eq 0 ]
run wait_for_container_status fedimint running 240
[ "$status" -eq 0 ]
}

View File

@ -0,0 +1,172 @@
#!/usr/bin/env bats
# tests/lifecycle/bats/mempool.bats
#
# Lifecycle tests for the mempool stack:
# - mempool (legacy install path; the frontend container)
# - mempool-api (orchestrator-managed; the backend api)
# - archy-mempool-db (orchestrator-managed; the mariadb)
# - archy-mempool-web (orchestrator-managed; the proxy/static layer)
#
# The mempool stack is split between the legacy install path (mempool itself)
# and orchestrator-managed sub-containers — see uses_orchestrator_install_flow
# in install.rs. Tests here treat them as one stack at the package.install/stop
# level, addressed by id "mempool". UI URL coverage is in ui-coverage.bats.
load '../lib/rpc.bash'
setup_file() {
: "${ARCHY_PASSWORD:?Set ARCHY_PASSWORD env var to the UI password}"
export ARCHY_FORCE_LOGIN=1
rpc_login
unset ARCHY_FORCE_LOGIN
}
teardown_file() {
rpc_logout_local
}
mempool_components=(
"mempool-api"
"archy-mempool-db"
)
mempool_optional_components=(
"mempool"
"archy-mempool-web"
)
mempool_skip_if_absent() {
for c in "${mempool_components[@]}"; do
podman inspect "$c" --format '{{.State.Status}}' >/dev/null 2>&1 && return 0
done
skip "mempool stack not installed"
}
@test "container-list includes the core mempool components" {
run rpc_result container-list
[ "$status" -eq 0 ]
local found=0
for c in "${mempool_components[@]}"; do
if echo "$output" | jq -e --arg n "$c" '.[] | select(.name == $n)' >/dev/null; then
found=$((found + 1))
fi
done
(( found > 0 )) || skip "mempool stack not installed"
}
@test "every present mempool component reports a valid state" {
run rpc_result container-list
[ "$status" -eq 0 ]
local present=0
for c in "${mempool_components[@]}" "${mempool_optional_components[@]}"; do
local state
state=$(echo "$output" | jq -r --arg n "$c" '.[] | select(.name == $n) | .state')
[[ -n "$state" ]] || continue
present=$((present + 1))
[[ "$state" =~ ^(running|stopped|exited|created|paused)$ ]] \
|| fail "invalid state for $c: $state"
done
(( present > 0 )) || skip "mempool stack not installed"
}
@test "no orphan mempool-related containers beyond the known set" {
local total known
total=$(podman ps -a --format '{{.Names}}' \
| grep -Ec '^(mempool|archy-mempool)' || true)
known=$(podman ps -a --format '{{.Names}}' \
| grep -Ec '^(mempool|mempool-api|archy-mempool-db|archy-mempool-web)$' || true)
[ "$total" -eq "$known" ]
}
# ────────────────────────────────────────────────────────────────────
# Destructive tier — operate on the package id "mempool" which the
# legacy install path treats as the whole stack
# ────────────────────────────────────────────────────────────────────
@test "package.stop transitions mempool stack to stopped" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
mempool_skip_if_absent
run rpc_result package.stop '{"id":"mempool"}'
[ "$status" -eq 0 ]
# The frontend container is the user-visible target; supporting
# services may stay running depending on orchestrator policy.
if podman inspect mempool --format '{{.State.Status}}' >/dev/null 2>&1; then
run wait_for_container_status mempool stopped 60
[ "$status" -eq 0 ]
fi
}
@test "package.start brings mempool stack back to running" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
mempool_skip_if_absent
run rpc_result package.start '{"id":"mempool"}'
[ "$status" -eq 0 ]
if podman inspect mempool --format '{{.State.Status}}' >/dev/null 2>&1; then
run wait_for_container_status mempool running 180
[ "$status" -eq 0 ]
fi
}
@test "package.restart leaves mempool stack in running state" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
mempool_skip_if_absent
run rpc_result package.restart '{"id":"mempool"}'
[ "$status" -eq 0 ]
if podman inspect mempool --format '{{.State.Status}}' >/dev/null 2>&1; then
run wait_for_container_status mempool running 180
[ "$status" -eq 0 ]
fi
}
@test "mempool api backend remains queryable when stack is up" {
[[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set"
mempool_skip_if_absent
# mempool-api on :8999 — same probe required-stack.bats uses for parity.
local deadline=$(( $(date +%s) + 60 ))
while (( $(date +%s) < deadline )); do
if curl -fsS -m 5 "http://127.0.0.1:8999/api/v1/backend-info" >/dev/null 2>&1; then
return 0
fi
sleep 3
done
fail "mempool-api never responded on :8999"
}
# ────────────────────────────────────────────────────────────────────
# Cascade-destructive tier
# ────────────────────────────────────────────────────────────────────
@test "package.uninstall removes the mempool stack" {
[[ "${ARCHY_ALLOW_CASCADE_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_CASCADE_DESTRUCTIVE not set"
mempool_skip_if_absent
run rpc_result package.uninstall '{"id":"mempool","preserve_data":true}'
[ "$status" -eq 0 ]
for c in "${mempool_components[@]}" "${mempool_optional_components[@]}"; do
if podman inspect "$c" --format '{{.State.Status}}' >/dev/null 2>&1; then
run wait_for_container_status "$c" absent 120
[ "$status" -eq 0 ] || fail "mempool component $c not removed by uninstall"
fi
done
}
@test "package.install restores the mempool stack" {
[[ "${ARCHY_ALLOW_CASCADE_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_CASCADE_DESTRUCTIVE not set"
run rpc_result package.install '{"manifest_path":"mempool/manifest.yaml"}'
[ "$status" -eq 0 ]
# At minimum the core orchestrator-managed components must come back.
for c in "${mempool_components[@]}"; do
run wait_for_container_status "$c" running 240
[ "$status" -eq 0 ] || fail "mempool component $c never reached running after reinstall"
done
}