From 5074572373c5e13733f6456916f4465017ae509f Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 1 May 2026 16:55:31 -0400 Subject: [PATCH] test(lifecycle): add btcpay + fedimint + mempool suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- tests/lifecycle/TESTING.md | 10 +- tests/lifecycle/bats/btcpay.bats | 146 ++++++++++++++++++++++++ tests/lifecycle/bats/fedimint.bats | 113 +++++++++++++++++++ tests/lifecycle/bats/mempool.bats | 172 +++++++++++++++++++++++++++++ 4 files changed, 436 insertions(+), 5 deletions(-) create mode 100644 tests/lifecycle/bats/btcpay.bats create mode 100644 tests/lifecycle/bats/fedimint.bats create mode 100644 tests/lifecycle/bats/mempool.bats diff --git a/tests/lifecycle/TESTING.md b/tests/lifecycle/TESTING.md index 0ef53835..1ffff506 100644 --- a/tests/lifecycle/TESTING.md +++ b/tests/lifecycle/TESTING.md @@ -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 | diff --git a/tests/lifecycle/bats/btcpay.bats b/tests/lifecycle/bats/btcpay.bats new file mode 100644 index 00000000..f7b605d4 --- /dev/null +++ b/tests/lifecycle/bats/btcpay.bats @@ -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 +} diff --git a/tests/lifecycle/bats/fedimint.bats b/tests/lifecycle/bats/fedimint.bats new file mode 100644 index 00000000..74d9c877 --- /dev/null +++ b/tests/lifecycle/bats/fedimint.bats @@ -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 ] +} diff --git a/tests/lifecycle/bats/mempool.bats b/tests/lifecycle/bats/mempool.bats new file mode 100644 index 00000000..494f48dd --- /dev/null +++ b/tests/lifecycle/bats/mempool.bats @@ -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 +}