#!/usr/bin/env bash # test-cross-node.sh — Master cross-node test suite for Archipelago # Runs all acceptance tests from BOTH directions (.228→.198 and .198→.228) # Usage: ./scripts/test-cross-node.sh [--iterations N] [--skip-reboot] # # Output: TAP format (Test Anything Protocol) # Exit 0 only if ALL tests pass ALL iterations from BOTH directions. set -euo pipefail # ── Config ────────────────────────────────────────────────────────────────── NODE_A="192.168.1.228" NODE_B="192.168.1.198" SSH_KEY="${HOME}/.ssh/archipelago-deploy" SSH_OPTS="-i ${SSH_KEY} -o StrictHostKeyChecking=no -o ConnectTimeout=10" ITERATIONS=10 SKIP_REBOOT=false SUDO_PASS="EwPDR8q45l0Upx@" PASS=0 FAIL=0 TEST_NUM=0 # ── Parse args ────────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --iterations) ITERATIONS="$2"; shift 2 ;; --skip-reboot) SKIP_REBOOT=true; shift ;; *) echo "Unknown arg: $1"; exit 1 ;; esac done # ── Helpers ───────────────────────────────────────────────────────────────── ssh_cmd() { local host="$1"; shift ssh ${SSH_OPTS} "archipelago@${host}" "$@" 2>/dev/null } ssh_sudo() { local host="$1"; shift ssh ${SSH_OPTS} "archipelago@${host}" "echo '${SUDO_PASS}' | sudo -S $*" 2>/dev/null } tap_ok() { TEST_NUM=$((TEST_NUM + 1)) PASS=$((PASS + 1)) echo "ok ${TEST_NUM} - $1" } tap_fail() { TEST_NUM=$((TEST_NUM + 1)) FAIL=$((FAIL + 1)) echo "not ok ${TEST_NUM} - $1" echo "# $2" } run_check() { local desc="$1" local result result=$(eval "$2" 2>/dev/null) || true if eval "$3" <<< "$result" >/dev/null 2>&1; then tap_ok "$desc" else tap_fail "$desc" "Got: ${result:-}" fi } # ── Auth helper ───────────────────────────────────────────────────────────── get_session() { local host="$1" curl -s -D- -o/dev/null -X POST \ -H "Content-Type: application/json" \ -d '{"method":"auth.login","params":{"password":"password123"}}' \ "http://${host}:5678/rpc/v1" 2>/dev/null | \ grep -i "set-cookie" | tr '\r' '\n' } rpc_call() { local host="$1" local method="$2" local session="$3" local csrf="$4" curl -s -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${session}; csrf_token=${csrf}" \ -H "X-CSRF-Token: ${csrf}" \ -d "{\"method\":\"${method}\"}" \ "http://${host}:5678/rpc/v1" 2>/dev/null } echo "TAP version 13" echo "# Archipelago Cross-Node Test Suite" echo "# Nodes: ${NODE_A} (A) ↔ ${NODE_B} (B)" echo "# Iterations: ${ITERATIONS}" echo "# Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "" # ═══════════════════════════════════════════════════════════════════════════ # US-01: System Health # ═══════════════════════════════════════════════════════════════════════════ echo "# --- US-01: System Health ---" for node in "$NODE_A" "$NODE_B"; do node_label=$([[ "$node" == "$NODE_A" ]] && echo "A(.228)" || echo "B(.198)") for i in $(seq 1 "$ITERATIONS"); do # Check 1: Health endpoint result=$(curl -s --connect-timeout 5 "http://${node}:5678/health" 2>/dev/null || echo "FAIL") if [[ "$result" == "OK" ]]; then tap_ok "US01-${node_label}-health-${i}" else tap_fail "US01-${node_label}-health-${i}" "Expected OK, got: ${result}" fi # Check 2: Services active svc_status=$(ssh_sudo "$node" "systemctl is-active archipelago nginx" 2>/dev/null | tr '\n' ' ') if echo "$svc_status" | grep -q "active active"; then tap_ok "US01-${node_label}-services-${i}" else tap_fail "US01-${node_label}-services-${i}" "Services: ${svc_status}" fi # Check 3: Memory available > 500MB (relaxed from 1GB given tight memory) avail_kb=$(ssh_cmd "$node" "grep MemAvailable /proc/meminfo | awk '{print \$2}'" 2>/dev/null) if [[ -n "$avail_kb" ]] && [[ "$avail_kb" -gt 512000 ]]; then tap_ok "US01-${node_label}-memory-${i} # available=${avail_kb}KB" else tap_fail "US01-${node_label}-memory-${i}" "Available: ${avail_kb:-unknown}KB (need >512000)" fi # Check 4: Load average < 2x cores cores=$(ssh_cmd "$node" "nproc" 2>/dev/null || echo "4") load_1m=$(ssh_cmd "$node" "awk '{print \$1}' /proc/loadavg" 2>/dev/null) max_load=$((cores * 2)) load_int=${load_1m%%.*} if [[ -n "$load_int" ]] && [[ "$load_int" -lt "$max_load" ]]; then tap_ok "US01-${node_label}-load-${i} # load=${load_1m}, cores=${cores}" else tap_fail "US01-${node_label}-load-${i}" "Load ${load_1m} >= ${max_load} (${cores} cores x 2)" fi # Check 5: Disk usage < 85% disk_pct=$(ssh_cmd "$node" "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null) if [[ -n "$disk_pct" ]] && [[ "$disk_pct" -lt 85 ]]; then tap_ok "US01-${node_label}-disk-${i} # ${disk_pct}%" else tap_fail "US01-${node_label}-disk-${i}" "Disk at ${disk_pct:-unknown}%" fi # Check 6: Zero exited containers exited=$(ssh_sudo "$node" "podman ps -a --format '{{.State}}' | grep -c -i exited" 2>/dev/null || echo "0") exited=$(echo "$exited" | tail -1 | tr -d '[:space:]') if [[ "$exited" == "0" ]]; then tap_ok "US01-${node_label}-containers-${i}" else tap_fail "US01-${node_label}-containers-${i}" "${exited} exited containers" fi done done # ═══════════════════════════════════════════════════════════════════════════ # US-02: Container Lifecycle # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "# --- US-02: Container Lifecycle ---" for node in "$NODE_A" "$NODE_B"; do node_label=$([[ "$node" == "$NODE_A" ]] && echo "A(.228)" || echo "B(.198)") for i in $(seq 1 "$ITERATIONS"); do # Check 1: All containers running (none exited) exited=$(ssh_sudo "$node" "podman ps -a --format '{{.State}}' | grep -c -i exited" 2>/dev/null || echo "0") exited=$(echo "$exited" | tail -1 | tr -d '[:space:]') if [[ "$exited" == "0" ]]; then tap_ok "US02-${node_label}-all-running-${i}" else tap_fail "US02-${node_label}-all-running-${i}" "${exited} exited containers" fi # Check 2: Container count matches expectations (>= 20) count=$(ssh_sudo "$node" "podman ps --format '{{.Names}}' | wc -l" 2>/dev/null | tail -1 | tr -d '[:space:]') if [[ -n "$count" ]] && [[ "$count" -ge 20 ]]; then tap_ok "US02-${node_label}-container-count-${i} # count=${count}" else tap_fail "US02-${node_label}-container-count-${i}" "Only ${count:-0} containers running (need >=20)" fi # Check 3: Health monitor auto-restart (stop filebrowser, wait, verify it restarts) # Only run this on first iteration to avoid disruption if [[ "$i" -eq 1 ]]; then # Stop filebrowser ssh_sudo "$node" "podman stop filebrowser" 2>/dev/null || true echo "# Stopped filebrowser on ${node_label}, waiting for health monitor to restart..." # Wait up to 90s for health monitor to restart it restarted=false for wait_i in $(seq 1 18); do sleep 5 fb_state=$(ssh_sudo "$node" "podman inspect filebrowser --format '{{.State.Status}}'" 2>/dev/null | tail -1 | tr -d '[:space:]') if [[ "$fb_state" == "running" ]]; then restarted=true break fi done if [[ "$restarted" == "true" ]]; then tap_ok "US02-${node_label}-health-restart-${i} # filebrowser restarted in $((wait_i * 5))s" else tap_fail "US02-${node_label}-health-restart-${i}" "filebrowser not restarted after 90s" # Manually restart to not leave it broken ssh_sudo "$node" "podman start filebrowser" 2>/dev/null || true fi else # For subsequent iterations, just verify filebrowser is running fb_state=$(ssh_sudo "$node" "podman inspect filebrowser --format '{{.State.Status}}'" 2>/dev/null | tail -1 | tr -d '[:space:]') if [[ "$fb_state" == "running" ]]; then tap_ok "US02-${node_label}-filebrowser-running-${i}" else tap_fail "US02-${node_label}-filebrowser-running-${i}" "filebrowser state: ${fb_state:-unknown}" fi fi done done # ═══════════════════════════════════════════════════════════════════════════ # US-05: Tor Hidden Services # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "# --- US-05: Tor Hidden Services ---" # Get onion addresses ONION_A=$(ssh_sudo "$NODE_A" "cat /var/lib/archipelago/tor/hidden_service_archipelago/hostname" 2>/dev/null | tail -1) ONION_B=$(ssh_sudo "$NODE_B" "cat /var/lib/tor/hidden_service_archipelago/hostname" 2>/dev/null | tail -1) echo "# Node A onion: ${ONION_A:-unknown}" echo "# Node B onion: ${ONION_B:-unknown}" for i in $(seq 1 "$ITERATIONS"); do # Test: .228 can reach .198 via Tor if [[ -n "$ONION_B" ]]; then tor_result=$(ssh_cmd "$NODE_A" "curl --socks5-hostname 127.0.0.1:9050 -s --connect-timeout 30 http://${ONION_B}/health" 2>/dev/null || echo "FAIL") if [[ "$tor_result" == "OK" ]]; then tap_ok "US05-A→B-tor-${i}" else tap_fail "US05-A→B-tor-${i}" "Got: ${tor_result}" fi else tap_fail "US05-A→B-tor-${i}" "No onion address for B" fi # Test: .198 can reach .228 via Tor if [[ -n "$ONION_A" ]]; then tor_result=$(ssh_cmd "$NODE_B" "curl --socks5-hostname 127.0.0.1:9050 -s --connect-timeout 30 http://${ONION_A}/health" 2>/dev/null || echo "FAIL") if [[ "$tor_result" == "OK" ]]; then tap_ok "US05-B→A-tor-${i}" else tap_fail "US05-B→A-tor-${i}" "Got: ${tor_result}" fi else tap_fail "US05-B→A-tor-${i}" "No onion address for A" fi done # ═══════════════════════════════════════════════════════════════════════════ # US-03: Federation Join (verify existing federation) # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "# --- US-03: Federation Join ---" for node in "$NODE_A" "$NODE_B"; do node_label=$([[ "$node" == "$NODE_A" ]] && echo "A(.228)" || echo "B(.198)") # Get session for RPC calls session_header=$(get_session "$node") session_val=$(echo "$session_header" | sed -n 's/.*session=\([^;]*\).*/\1/p') csrf_val=$(echo "$session_header" | sed -n 's/.*csrf_token=\([^;]*\).*/\1/p') for i in $(seq 1 "$ITERATIONS"); do # Call federation.list-nodes fed_result=$(rpc_call "$node" "federation.list-nodes" "$session_val" "$csrf_val") # Check 1: At least 1 peer present peer_count=$(echo "$fed_result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('result',{}).get('nodes',[])))" 2>/dev/null || echo "0") if [[ "$peer_count" -ge 1 ]]; then tap_ok "US03-${node_label}-peers-present-${i} # count=${peer_count}" else tap_fail "US03-${node_label}-peers-present-${i}" "No federation peers found" fi # Check 2: Trust level is 'trusted' trust=$(echo "$fed_result" | python3 -c "import sys,json; d=json.load(sys.stdin); nodes=d.get('result',{}).get('nodes',[]); print(nodes[0].get('trust_level','') if nodes else '')" 2>/dev/null || echo "") if [[ "$trust" == "trusted" ]]; then tap_ok "US03-${node_label}-trust-level-${i}" else tap_fail "US03-${node_label}-trust-level-${i}" "Trust level: ${trust:-unknown}" fi # Check 3: DID present did=$(echo "$fed_result" | python3 -c "import sys,json; d=json.load(sys.stdin); nodes=d.get('result',{}).get('nodes',[]); print(nodes[0].get('did','') if nodes else '')" 2>/dev/null || echo "") if [[ -n "$did" ]] && [[ "$did" == did:* ]]; then tap_ok "US03-${node_label}-did-present-${i}" else tap_fail "US03-${node_label}-did-present-${i}" "DID: ${did:-missing}" fi # Check 4: last_seen within 10 minutes last_seen=$(echo "$fed_result" | python3 -c " import sys,json from datetime import datetime, timezone, timedelta d=json.load(sys.stdin) nodes=d.get('result',{}).get('nodes',[]) if not nodes: print('missing'); sys.exit() ls = nodes[0].get('last_seen','') if not ls: print('never'); sys.exit() try: dt = datetime.fromisoformat(ls.replace('Z','+00:00')) diff = datetime.now(timezone.utc) - dt print('ok' if diff < timedelta(minutes=10) else f'stale:{diff}') except: print('parse_error') " 2>/dev/null || echo "error") if [[ "$last_seen" == "ok" ]]; then tap_ok "US03-${node_label}-last-seen-${i}" else tap_fail "US03-${node_label}-last-seen-${i}" "last_seen: ${last_seen}" fi done done # ═══════════════════════════════════════════════════════════════════════════ # US-04: Federation Sync # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "# --- US-04: Federation Sync ---" for node in "$NODE_A" "$NODE_B"; do node_label=$([[ "$node" == "$NODE_A" ]] && echo "A(.228)" || echo "B(.198)") session_header=$(get_session "$node") session_val=$(echo "$session_header" | sed -n 's/.*session=\([^;]*\).*/\1/p') csrf_val=$(echo "$session_header" | sed -n 's/.*csrf_token=\([^;]*\).*/\1/p') for i in $(seq 1 "$ITERATIONS"); do # Trigger sync sync_result=$(curl -s -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${session_val}; csrf_token=${csrf_val}" \ -H "X-CSRF-Token: ${csrf_val}" \ -d '{"method":"federation.sync-state"}' \ "http://${node}:5678/rpc/v1" 2>/dev/null) # Check 1: Sync returns results has_results=$(echo "$sync_result" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); print('ok' if r and 'results' in r else 'no')" 2>/dev/null || echo "error") if [[ "$has_results" == "ok" ]]; then tap_ok "US04-${node_label}-sync-returns-${i}" else tap_fail "US04-${node_label}-sync-returns-${i}" "No sync results" fi # Check 2: At least one sync target succeeded sync_ok=$(echo "$sync_result" | python3 -c "import sys,json; d=json.load(sys.stdin); results=d.get('result',{}).get('results',[]); ok=[r for r in results if r.get('status')=='ok']; print(len(ok))" 2>/dev/null || echo "0") if [[ "$sync_ok" -ge 1 ]]; then tap_ok "US04-${node_label}-sync-success-${i} # ok=${sync_ok}" else tap_fail "US04-${node_label}-sync-success-${i}" "No successful syncs" fi # Check 3: Synced node has apps list apps_count=$(echo "$sync_result" | python3 -c "import sys,json; d=json.load(sys.stdin); results=d.get('result',{}).get('results',[]); ok=[r for r in results if r.get('status')=='ok']; print(ok[0].get('apps',0) if ok else 0)" 2>/dev/null || echo "0") if [[ "$apps_count" -gt 0 ]]; then tap_ok "US04-${node_label}-sync-apps-${i} # apps=${apps_count}" else tap_fail "US04-${node_label}-sync-apps-${i}" "Synced app count: ${apps_count}" fi # Check 4: last_seen updated after sync (re-check federation list) fed_after=$(rpc_call "$node" "federation.list-nodes" "$session_val" "$csrf_val") ls_fresh=$(echo "$fed_after" | python3 -c " import sys,json from datetime import datetime, timezone, timedelta d=json.load(sys.stdin) nodes=d.get('result',{}).get('nodes',[]) if not nodes: print('missing'); sys.exit() ls = nodes[0].get('last_seen','') if not ls: print('never'); sys.exit() try: dt = datetime.fromisoformat(ls.replace('Z','+00:00')) diff = datetime.now(timezone.utc) - dt print('ok' if diff < timedelta(minutes=2) else f'stale:{diff}') except: print('parse_error') " 2>/dev/null || echo "error") if [[ "$ls_fresh" == "ok" ]]; then tap_ok "US04-${node_label}-last-seen-fresh-${i}" else tap_fail "US04-${node_label}-last-seen-fresh-${i}" "last_seen after sync: ${ls_fresh}" fi done done # ═══════════════════════════════════════════════════════════════════════════ # US-07: File Sharing # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "# --- US-07: File Sharing ---" # Create a test file on both nodes for sharing (use bash -c to keep entire command under sudo) ssh_sudo "$NODE_A" "bash -c 'mkdir -p /var/lib/archipelago/content/files && echo test-content-from-228 > /var/lib/archipelago/content/files/test-share.txt && chown -R archipelago:archipelago /var/lib/archipelago/content'" 2>/dev/null || true ssh_sudo "$NODE_B" "bash -c 'mkdir -p /var/lib/archipelago/content/files && echo test-content-from-198 > /var/lib/archipelago/content/files/test-share-b.txt && chown -R archipelago:archipelago /var/lib/archipelago/content'" 2>/dev/null || true for i in $(seq 1 "$ITERATIONS"); do # --- .228 shares content, .198 browses --- # Get .228 auth session_header_a=$(get_session "$NODE_A") session_a=$(echo "$session_header_a" | sed -n 's/.*session=\([^;]*\).*/\1/p') csrf_a=$(echo "$session_header_a" | sed -n 's/.*csrf_token=\([^;]*\).*/\1/p') # Add content on .228 add_result=$(curl -s -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${session_a}; csrf_token=${csrf_a}" \ -H "X-CSRF-Token: ${csrf_a}" \ -d '{"method":"content.add","params":{"filename":"test-share.txt","mime_type":"text/plain","description":"Test share from 228"}}' \ "http://${NODE_A}:5678/rpc/v1" 2>/dev/null) has_item=$(echo "$add_result" | python3 -c "import sys,json; d=json.load(sys.stdin); print('ok' if d.get('result',{}).get('item') else 'no')" 2>/dev/null || echo "error") if [[ "$has_item" == "ok" ]]; then tap_ok "US07-A-content-add-${i}" else tap_fail "US07-A-content-add-${i}" "content.add failed: ${add_result:0:80}" fi # List content on .228 list_result=$(curl -s -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${session_a}; csrf_token=${csrf_a}" \ -H "X-CSRF-Token: ${csrf_a}" \ -d '{"method":"content.list-mine"}' \ "http://${NODE_A}:5678/rpc/v1" 2>/dev/null) item_count=$(echo "$list_result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('result',{}).get('items',[])))" 2>/dev/null || echo "0") if [[ "$item_count" -gt 0 ]]; then tap_ok "US07-A-content-listed-${i} # items=${item_count}" else tap_fail "US07-A-content-listed-${i}" "No items in catalog" fi # Browse .228's catalog from .198 over Tor session_header_b=$(get_session "$NODE_B") session_b=$(echo "$session_header_b" | sed -n 's/.*session=\([^;]*\).*/\1/p') csrf_b=$(echo "$session_header_b" | sed -n 's/.*csrf_token=\([^;]*\).*/\1/p') browse_result=$(curl -s --max-time 45 -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${session_b}; csrf_token=${csrf_b}" \ -H "X-CSRF-Token: ${csrf_b}" \ -d "{\"method\":\"content.browse-peer\",\"params\":{\"onion\":\"${ONION_A}\"}}" \ "http://${NODE_B}:5678/rpc/v1" 2>/dev/null) peer_items=$(echo "$browse_result" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); items=r.get('items',[]); print(len(items))" 2>/dev/null || echo "0") if [[ "$peer_items" -gt 0 ]]; then tap_ok "US07-B-browse-A-${i} # items=${peer_items}" else tap_fail "US07-B-browse-A-${i}" "Could not browse .228 catalog: ${browse_result:0:80}" fi # --- Reverse: .198 shares, .228 browses --- add_result_b=$(curl -s -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${session_b}; csrf_token=${csrf_b}" \ -H "X-CSRF-Token: ${csrf_b}" \ -d '{"method":"content.add","params":{"filename":"test-share-b.txt","mime_type":"text/plain","description":"Test share from 198"}}' \ "http://${NODE_B}:5678/rpc/v1" 2>/dev/null) has_item_b=$(echo "$add_result_b" | python3 -c "import sys,json; d=json.load(sys.stdin); print('ok' if d.get('result',{}).get('item') else 'no')" 2>/dev/null || echo "error") if [[ "$has_item_b" == "ok" ]]; then tap_ok "US07-B-content-add-${i}" else tap_fail "US07-B-content-add-${i}" "content.add failed on .198" fi # Browse .198's catalog from .228 over Tor browse_result_a=$(curl -s --max-time 45 -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${session_a}; csrf_token=${csrf_a}" \ -H "X-CSRF-Token: ${csrf_a}" \ -d "{\"method\":\"content.browse-peer\",\"params\":{\"onion\":\"${ONION_B}\"}}" \ "http://${NODE_A}:5678/rpc/v1" 2>/dev/null) peer_items_a=$(echo "$browse_result_a" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); items=r.get('items',[]); print(len(items))" 2>/dev/null || echo "0") if [[ "$peer_items_a" -gt 0 ]]; then tap_ok "US07-A-browse-B-${i} # items=${peer_items_a}" else tap_fail "US07-A-browse-B-${i}" "Could not browse .198 catalog: ${browse_result_a:0:80}" fi done # Clean up test content entries (remove duplicates) for node in "$NODE_A" "$NODE_B"; do session_header=$(get_session "$node") sv=$(echo "$session_header" | sed -n 's/.*session=\([^;]*\).*/\1/p') cv=$(echo "$session_header" | sed -n 's/.*csrf_token=\([^;]*\).*/\1/p') # Get all items and remove test ones items_json=$(curl -s -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${sv}; csrf_token=${cv}" \ -H "X-CSRF-Token: ${cv}" \ -d '{"method":"content.list-mine"}' \ "http://${node}:5678/rpc/v1" 2>/dev/null) echo "$items_json" | python3 -c " import sys,json d=json.load(sys.stdin) items=d.get('result',{}).get('items',[]) test_items=[i['id'] for i in items if 'test-share' in i.get('filename','')] for tid in test_items: print(tid) " 2>/dev/null | while read -r tid; do curl -s -X POST \ -H "Content-Type: application/json" \ -H "Cookie: session=${sv}; csrf_token=${cv}" \ -H "X-CSRF-Token: ${cv}" \ -d "{\"method\":\"content.remove\",\"params\":{\"id\":\"${tid}\"}}" \ "http://${node}:5678/rpc/v1" >/dev/null 2>&1 done done # ═══════════════════════════════════════════════════════════════════════════ # US-09: NIP-07 Signing # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "# --- US-09: NIP-07 Signing ---" for node in "$NODE_A" "$NODE_B"; do node_label=$([[ "$node" == "$NODE_A" ]] && echo "A(.228)" || echo "B(.198)") for i in $(seq 1 "$ITERATIONS"); do # Check: nostr-provider.js injected in app pages provider=$(curl -s --connect-timeout 5 "http://${node}/app/mempool/" 2>/dev/null | grep -c "nostr-provider" || echo "0") if [[ "$provider" -gt 0 ]]; then tap_ok "US09-${node_label}-provider-${i}" else tap_fail "US09-${node_label}-provider-${i}" "nostr-provider.js not found in /app/mempool/" fi done done # ═══════════════════════════════════════════════════════════════════════════ # Summary # ═══════════════════════════════════════════════════════════════════════════ echo "" TOTAL=$((PASS + FAIL)) echo "1..${TOTAL}" echo "" echo "# ═══════════════════════════════════════════════════════════════" echo "# Results: ${PASS} passed, ${FAIL} failed, ${TOTAL} total" echo "# Finished: $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "# ═══════════════════════════════════════════════════════════════" if [[ "$FAIL" -gt 0 ]]; then exit 1 fi exit 0