#!/bin/bash # Fixture-only test for node-facts.sh # Tests parsing/assembly functions with canned JSON responses # Must never open network connections BASEPATH="$(dirname "$0")" # Send heartbeat to prevent watchdog kill touch "/root/.mcp-jobs/vibe-node-changes/20261006-070518-re-dispatch-of-a-change-a-prior-run-alre-3723628/heartbeat" 2>/dev/null || true echo "=== Testing node-facts.sh (fixture-only) ===" # Test 1: Check that the script exists and is executable if [ ! -x "$BASEPATH/node-facts.sh" ]; then echo "FAIL: node-facts.sh is not executable" exit 1 else echo "PASS: node-facts.sh is executable" fi # Test 2: Check syntax bash -n "$BASEPATH/node-facts.sh" && echo "PASS: Script syntax is valid" || { echo "FAIL: Script syntax error"; exit 1; } # Test 3: Check that it exits with code 2 when .env is missing TMP_DIR=$(mktemp -d) cp "$BASEPATH/node-facts.sh" "$TMP_DIR/" cd "$TMP_DIR" || exit 1 output=$(./node-facts.sh 2>&1) exit_code=$? if [ $exit_code -eq 2 ] && echo "$output" | grep -q "cannot read .env"; then echo "PASS: Exits with code 2 when .env is missing with JSON error" else echo "FAIL: Expected exit code 2 with JSON error, got $exit_code with output: $output" rm -rf "$TMP_DIR" exit 1 fi # Test 4: Check that it exits with code 2 when COMPOSE_FILE is empty echo "NO_SSL=true" > .env output=$(./node-facts.sh 2>&1) exit_code=$? if [ $exit_code -eq 2 ] && echo "$output" | grep -q "COMPOSE_FILE is empty"; then echo "PASS: Exits with code 2 when COMPOSE_FILE is empty with JSON error" else echo "FAIL: Expected exit code 2 with JSON error, got $exit_code with output: $output" rm -rf "$TMP_DIR" exit 1 fi # Test 5: Test fail-fast completes within 1 second START_TIME=$(date +%s) output=$(timeout 1 ./node-facts.sh 2>&1) END_TIME=$(date +%s) exit_code=$? if [ $((END_TIME - START_TIME)) -le 1 ]; then echo "PASS: Script fails fast (within 1 second) when .env is missing" else echo "FAIL: Script took too long to fail" rm -rf "$TMP_DIR" exit 1 fi rm -rf "$TMP_DIR" echo "=== Testing stub-curl integration ===" # Stub-curl test: test with fixture compose directory and stub curl STUB_TEST_DIR="$BASEPATH/node-facts-test" if [ ! -d "$STUB_TEST_DIR" ]; then echo "SKIP: Stub test directory not found" else # Test 6: Test with stub curl - single node mode TMP_DIR=$(mktemp -d) cd "$TMP_DIR" || exit 1 # Copy test files cp -r "$STUB_TEST_DIR"/* . # Make stub curl executable and put it first in PATH STUB_CURL_DIR="$TMP_DIR/stub-curl" chmod +x "$STUB_CURL_DIR/curl" # Test single node mode with stub curl ORIGINAL_PATH="$PATH" export PATH="$STUB_CURL_DIR:$PATH" START_TIME=$(date +%s) output=$(timeout 60 ./node-facts.sh "avalanche-mainnet-go" 2>&1) END_TIME=$(date +%s) exit_code=$? export PATH="$ORIGINAL_PATH" if [ $exit_code -eq 0 ] && [ $((END_TIME - START_TIME)) -lt 10 ]; then echo "PASS: Stub curl single node mode exits 0 and completes in < 10s" # Validate it's JSON and has the right schema if echo "$output" | jq -e '.schema, .node_path, .network, .client' >/dev/null 2>&1; then echo "PASS: Single node output is valid JSON with required schema fields" else echo "FAIL: Single node output is not valid JSON schema" echo "Output: $output" rm -rf "$TMP_DIR" exit 1 fi else echo "FAIL: Stub curl single node mode failed with exit code $exit_code, took $((END_TIME - START_TIME))s" echo "Output: $output" rm -rf "$TMP_DIR" exit 1 fi # Test 7: Test with stub curl - all nodes mode START_TIME=$(date +%s) output=$(timeout 60 ./node-facts.sh 2>&1) END_TIME=$(date +%s) exit_code=$? if [ $exit_code -eq 0 ] && [ $((END_TIME - START_TIME)) -lt 10 ]; then echo "PASS: Stub curl all-nodes mode exits 0 and completes in < 10s" # Count JSON lines - should have at least 3 valid JSON lines (one per node) json_lines=$(echo "$output" | grep -c '^{.*}$' || echo "0") if [ "$json_lines" -ge 3 ]; then echo "PASS: All-nodes mode produced $json_lines JSON lines (>= 3)" else echo "FAIL: All-nodes mode produced only $json_lines JSON lines, expected >= 3" echo "Output: $output" rm -rf "$TMP_DIR" exit 1 fi # Validate each JSON line against schema valid_count=0 while IFS= read -r line; do if echo "$line" | jq -e '.schema, .at, .host, .node_path, .network, .client, .version, .peers, .schedule, .signals, .head, .timing_ms' >/dev/null 2>&1; then valid_count=$((valid_count + 1)) else echo "Invalid JSON line: $line" fi done <<< "$(echo "$output" | grep '^{.*}$')" if [ $valid_count -ge 3 ]; then echo "PASS: All $valid_count JSON lines have valid schema v1 fields" else echo "FAIL: Only $valid_count valid JSON lines out of $json_lines total" rm -rf "$TMP_DIR" exit 1 fi # Check for no stderr noise (grep/sed/jq errors) if echo "$output" | grep -q "grep: invalid argument\|local: can only be used\|sed:\|jq:"; then echo "FAIL: Found stderr noise in output: $(echo "$output" | grep "grep: invalid argument\|local: can only be used\|sed:\|jq:")" rm -rf "$TMP_DIR" exit 1 else echo "PASS: No stderr noise (grep/sed/jq errors) in output" fi else echo "FAIL: Stub curl all-nodes mode failed with exit code $exit_code, took $((END_TIME - START_TIME))s" echo "Output: $output" rm -rf "$TMP_DIR" exit 1 fi # Test 8: Test that path-blacklist with - prefix works # Add a compose file that should be excluded by the -node blacklist entry echo "version: '3'" > test-node-blacklist.yml echo "services:" >> test-node-blacklist.yml echo " client:" >> test-node-blacklist.yml echo " image: test/node-client:v1.0.0" >> test-node-blacklist.yml echo "chain: test-node-blacklist" >> test-node-blacklist.yml # Update COMPOSE_FILE to include the blacklisted file export COMPOSE_FILE="avalanche-mainnet-go.yml:test-node-blacklist.yml" output=$(timeout 60 ./node-facts.sh 2>&1) exit_code=$? if [ $exit_code -eq 0 ]; then # Should not include test-node-blacklist if echo "$output" | grep -q "test-node-blacklist"; then echo "FAIL: Blacklisted node was included in output" rm -rf "$TMP_DIR" exit 1 else echo "PASS: Blacklist with - prefix works correctly" fi else echo "FAIL: Blacklist test failed with exit code $exit_code" rm -rf "$TMP_DIR" exit 1 fi rm -rf "$TMP_DIR" fi echo "=== Testing JSON schema validation ===" # Function to validate schema validate_schema() { local json_file="$1" # Check all required top-level fields exist jq -e '.schema, .at, .host, .node_path, .network, .client, .version, .peers, .schedule, .signals, .head, .timing_ms' "$json_file" >/dev/null 2>&1 } # Test with fixture JSON files TMP_DIR=$(mktemp -d) # Test avalanche schema cat > "$TMP_DIR/avalanche.json" << 'EOF' { "schema": 1, "at": "2026-10-06T06:00:00Z", "host": "test-host", "node_path": "avalanche-mainnet-go", "network": "avalanche", "client": {"name": "avalanchego", "image": "ava-labs/avalanchego", "tag": "v1.10.0", "container": "avalanche-mainnet-go_client_1"}, "version": {"reported": "1.10.0", "db_version": "1.0.0", "source": "info.getNodeVersion", "error": null}, "peers": {"count": 50, "versions": {"1.10.0": 25, "1.9.0": 15}, "newer_than_us": 0, "source": "info.peers", "error": null}, "schedule": {"upgrades": {"apricotPhase1Time": {"time": "2021-04-27T00:00:00Z", "block": 4200000}}, "source": "info.upgrades", "error": null}, "signals": {"acp123": {"supporter": 30, "objector": 5}, "source": "info.acps", "error": null}, "head": {"height": null, "timestamp": null, "syncing": null, "source": "info.getNodeVersion", "error": "avalanche head not implemented"}, "timing_ms": {"version": 45, "peers": 120, "schedule": 25, "head": 5} } EOF if validate_schema "$TMP_DIR/avalanche.json"; then echo "PASS: Avalanche schema is valid" else echo "FAIL: Avalanche schema validation failed" rm -rf "$TMP_DIR" exit 1 fi # Test beacon schema cat > "$TMP_DIR/beacon.json" << 'EOF' { "schema": 1, "at": "2026-10-06T06:00:00Z", "host": "test-host", "node_path": "ethereum-mainnet-lighthouse", "network": "ethereum", "client": {"name": "beacon", "image": "sigp/lighthouse", "tag": "v5.3.0", "container": "ethereum-mainnet-lighthouse_client_1"}, "version": {"reported": "Lighthouse/v5.3.0", "db_version": null, "source": "eth/v1/node/version", "error": null}, "peers": {"count": 25, "versions": null, "newer_than_us": null, "source": "eth/v1/node/peers", "error": "beacon peers API carries no agent version"}, "schedule": {"upgrades": {"ALTAIR_FORK_EPOCH": {"block": 74240, "time": null}}, "source": "eth/v1/config/spec", "error": null}, "signals": null, "head": {"height": 12345678, "timestamp": "2026-10-06T05:59:59Z", "syncing": false, "source": "eth/v1/node/syncing", "error": null}, "timing_ms": {"version": 30, "peers": 40, "schedule": 20, "head": 50} } EOF if validate_schema "$TMP_DIR/beacon.json"; then echo "PASS: Beacon schema is valid" else echo "FAIL: Beacon schema validation failed" rm -rf "$TMP_DIR" exit 1 fi # Test geth schema cat > "$TMP_DIR/geth.json" << 'EOF' { "schema": 1, "at": "2026-10-06T06:00:00Z", "host": "test-host", "node_path": "ethereum-mainnet-geth-pruned", "network": "ethereum", "client": {"name": "geth", "image": "ethereum/client-go", "tag": "v1.13.0", "container": "ethereum-mainnet-geth-pruned_client_1"}, "version": {"reported": "Geth/v1.13.0-stable", "db_version": null, "source": "web3_clientVersion", "error": null}, "peers": {"count": 50, "versions": {"Geth/v1.13.0": 25, "Geth/v1.12.0": 15}, "newer_than_us": 5, "source": "admin_peers", "error": null}, "schedule": {"upgrades": {"byzantiumBlock": {"block": 4370000, "time": null}}, "source": "admin_nodeInfo", "error": null}, "signals": null, "head": {"height": 21000000, "timestamp": "2026-10-06T05:59:59Z", "syncing": false, "source": "eth_blockNumber", "error": null}, "timing_ms": {"version": 20, "peers": 80, "schedule": 30, "head": 60} } EOF if validate_schema "$TMP_DIR/geth.json"; then echo "PASS: Geth schema is valid" else echo "FAIL: Geth schema validation failed" rm -rf "$TMP_DIR" exit 1 fi # Test op-node schema cat > "$TMP_DIR/opnode.json" << 'EOF' { "schema": 1, "at": "2026-10-06T06:00:00Z", "host": "test-host", "node_path": "op-base-mainnet", "network": "base", "client": {"name": "op-node", "image": "us-docker.pkg.dev/oplabs/op-node", "tag": "v1.19.0", "container": "op-base-mainnet_node_1"}, "version": {"reported": "v1.19.0", "db_version": null, "source": "optimism_version", "error": null}, "peers": {"count": 10, "versions": {"op-node/v1.19.0": 5}, "newer_than_us": 0, "source": "opp2p_peers", "error": null}, "schedule": {"upgrades": {"canyon_time": {"time": "2023-08-10T00:00:00Z", "block": null}}, "source": "optimism_rollupConfig", "error": null}, "signals": null, "head": {"height": 12345678, "timestamp": "2026-10-06T05:59:59Z", "syncing": false, "source": "optimism_syncStatus", "error": null}, "timing_ms": {"version": 25, "peers": 90, "schedule": 35, "head": 45} } EOF if validate_schema "$TMP_DIR/opnode.json"; then echo "PASS: OP-Node schema is valid" else echo "FAIL: OP-Node schema validation failed" rm -rf "$TMP_DIR" exit 1 fi # Test reth schema (with null schedule) cat > "$TMP_DIR/reth.json" << 'EOF' { "schema": 1, "at": "2026-10-06T06:00:00Z", "host": "test-host", "node_path": "base-mainnet-reth", "network": "base", "client": {"name": "reth", "image": "ghcr.io/base/node-reth", "tag": "v1.0.0", "container": "base-mainnet-reth_client_1"}, "version": {"reported": "reth/v1.0.0", "db_version": null, "source": "clientVersion", "error": null}, "peers": {"count": 20, "versions": {"reth/v1.0.0": 15}, "newer_than_us": 2, "source": "admin_peers", "error": null}, "schedule": {"upgrades": null, "source": null, "error": "reth exposes no chain config over RPC"}, "signals": null, "head": {"height": 12345678, "timestamp": "2026-10-06T05:59:59Z", "syncing": false, "source": "eth_blockNumber", "error": null}, "timing_ms": {"version": 15, "peers": 60, "schedule": 5, "head": 30} } EOF if validate_schema "$TMP_DIR/reth.json"; then echo "PASS: Reth schema is valid" else echo "FAIL: Reth schema validation failed" rm -rf "$TMP_DIR" exit 1 fi # Clean up rm -rf "$TMP_DIR" # Send final heartbeat touch "/root/.mcp-jobs/vibe-node-changes/20261006-070518-re-dispatch-of-a-change-a-prior-run-alre-3723628/heartbeat" 2>/dev/null || true echo "SUCCESS: All fixture tests passed" exit 0