diff --git a/tests/lifecycle/bats/electrumx.bats b/tests/lifecycle/bats/electrumx.bats index 98ff38e6..05d7f2ff 100644 --- a/tests/lifecycle/bats/electrumx.bats +++ b/tests/lifecycle/bats/electrumx.bats @@ -27,6 +27,38 @@ teardown_file() { rpc_logout_local } +# How far electrumx is behind its bitcoin daemon, from its own recent log +# ("our height: N ... daemon: M"). Prints the gap; prints nothing when no +# fresh (<30 min) sync line exists — callers must treat that as "unknown", +# never as "synced". +# +# Why this exists: ElectrumX serves NO sessions until its initial sync has +# caught up, and it flushes its DB cache at 1GB — i.e. rarely — so every +# restart discards all unflushed progress back to the last flush. On +# 2026-08-09 this node had spent 8d14h syncing largely because gate runs and +# reboots kept taking its progress away. A gate that stop/start/restarts a +# mid-sync electrumx therefore (a) honestly fails the serving probe and +# (b) actively destroys hours of sync work. Skipping WITH THE GAP NAMED is +# the truthful behaviour — this is a positively-detected syncing state, not +# the container-absent skip trap fixed earlier in this file's history. +electrumx_sync_gap() { + local line ours daemon + line=$(podman logs --tail 400 --since 30m electrumx 2>/dev/null \ + | grep -E 'our height: [0-9,]+ daemon: [0-9,]+' | tail -1) + [[ -z "$line" ]] && return 0 + ours=$(echo "$line" | grep -oE 'our height: [0-9,]+' | tr -dc '0-9') + daemon=$(echo "$line" | grep -oE 'daemon: [0-9,]+' | tr -dc '0-9') + [[ -n "$ours" && -n "$daemon" ]] && echo $((daemon - ours)) +} + +skip_if_initial_sync() { + local gap + gap=$(electrumx_sync_gap) + if [[ -n "$gap" ]] && (( gap > 10 )); then + skip "electrumx initial sync in progress ($gap blocks behind) — $1" + fi +} + # ──────────────────────────────────────────────────────────────────── # Read-only tier # ──────────────────────────────────────────────────────────────────── @@ -52,6 +84,10 @@ teardown_file() { skip "electrumx not running (state=$state)" fi + # ElectrumX serves no sessions until initial sync completes, so probing a + # mid-sync instance can only fail — skip with the gap named instead. + skip_if_initial_sync "sessions are not served until it catches up" + # Same probe required-stack.bats uses — divergence flags a real regression. # It is a real Electrum round-trip, not a bare connect(): podman's port # forwarder accepts the TCP handshake on the host-published port even when @@ -96,6 +132,7 @@ PY @test "package.stop transitions electrumx to stopped" { [[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set" + skip_if_initial_sync "restarting discards unflushed sync progress (1GB flush cache)" run rpc_result package.stop '{"id":"electrumx"}' [ "$status" -eq 0 ] @@ -106,6 +143,7 @@ PY @test "package.start brings electrumx back to running" { [[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set" + skip_if_initial_sync "restarting discards unflushed sync progress (1GB flush cache)" run rpc_result package.start '{"id":"electrumx"}' [ "$status" -eq 0 ] @@ -116,6 +154,7 @@ PY @test "package.restart leaves electrumx in running state" { [[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set" + skip_if_initial_sync "restarting discards unflushed sync progress (1GB flush cache)" run rpc_result package.restart '{"id":"electrumx"}' [ "$status" -eq 0 ] @@ -126,6 +165,7 @@ PY @test "electrumx TCP port recovers after restart" { [[ "${ARCHY_ALLOW_DESTRUCTIVE:-0}" == "1" ]] || skip "ARCHY_ALLOW_DESTRUCTIVE not set" + skip_if_initial_sync "restarting discards unflushed sync progress (1GB flush cache)" # electrumx replays its index against bitcoind on cold start; allow 120s. local deadline=$(( $(date +%s) + 120 )) diff --git a/tests/lifecycle/bats/required-stack.bats b/tests/lifecycle/bats/required-stack.bats index a52662f4..4a199c70 100644 --- a/tests/lifecycle/bats/required-stack.bats +++ b/tests/lifecycle/bats/required-stack.bats @@ -50,6 +50,24 @@ skip_if_not_installed() { container_installed "$1" || skip "$1 not installed on this node" } +# ElectrumX serves no sessions until its initial sync completes, so the +# protocol probe below can only fail mid-sync. Skip with the gap NAMED (a +# positively-detected state from a fresh log line, never inferred from +# absence). Canonical rationale lives in electrumx.bats next to its twin. +skip_if_electrumx_initial_sync() { + local line ours daemon + line=$(podman logs --tail 400 --since 30m electrumx 2>/dev/null \ + | grep -E 'our height: [0-9,]+ daemon: [0-9,]+' | tail -1) + [[ -z "$line" ]] && return 0 + ours=$(echo "$line" | grep -oE 'our height: [0-9,]+' | tr -dc '0-9') + daemon=$(echo "$line" | grep -oE 'daemon: [0-9,]+' | tr -dc '0-9') + [[ -n "$ours" && -n "$daemon" ]] || return 0 + local gap=$((daemon - ours)) + if (( gap > 10 )); then + skip "electrumx initial sync in progress ($gap blocks behind) — sessions are not served until it catches up" + fi +} + # The subset of required_containers actually installed on this node. installed_required_containers() { local c @@ -135,6 +153,7 @@ bitcoin_json() { @test "electrumx answers the Electrum protocol (not just an open socket)" { skip_if_not_installed electrumx + skip_if_electrumx_initial_sync # A bare connect() to the HOST-published port proves nothing: podman's port # forwarder accepts the TCP handshake even when nothing inside the container # is listening. On 2026-08-09 this test was green while mempool-api was in a