diff --git a/tests/README.md b/tests/README.md index ac02643..469e296 100644 --- a/tests/README.md +++ b/tests/README.md @@ -18,7 +18,7 @@ compiles XMRig and mines. Each layer covers what the one below it has to stub. | **Coverage gate** | [`coverage.sh`](coverage.sh) | kcov in **Docker**. **In CI.** | Line coverage of `rigforge.sh` + `util/proposed-grub.sh` by running `run.sh` under kcov; enforces the committed floor ([`coverage-floor.txt`](coverage-floor.txt)) plus a patch-coverage gate (diff-cover) on changed lines. | `make coverage` | | **Release smoke (quick)** | [`smoke.sh`](smoke.sh) | Real Linux rig, **manual**. Not in CI. | The compiled binary actually starts and hashes (`xmrig --bench`, fully offline). Fast pre-tag confidence that the worker we ship runs. | `make smoke` | | **Release e2e (full)** | [`e2e-real.sh`](e2e-real.sh) | Real Linux rig, **manual, root**. Not in CI. | The real thing end to end: build + tune + kernel tuning + service + a real hash, the writable control path (#236) against real systemd (`control` phase), the remote-upgrade chain with real git as the root oneshot (`upgrade` phase, #322), then a clean uninstall. **The release gate.** | `make e2e-real` (see [`RELEASING.md`](../RELEASING.md)) | -| **Release e2e (worker↔stack)** | [`e2e-pithead.sh`](e2e-pithead.sh) ([`control` phase](e2e-pithead-control.sh), [`access-tokens` phase](e2e-pithead-tokens.sh)) | Real rig + a **live Pithead stack**, **manual, root**. Not in CI (`connect`/`control`/`access-tokens` also run as a `rigforge`/`tier4-e2e` bench-ci job). | The integration contract: mining round-trip against the stack, the `:8080` API posture (open/token/`restricted`), the writable control path's DONATION apply round trip (#344/#509), a named `ACCESS_TOKENS` entry authenticating `:8081`/`:8082` while staying refused on `:8080` and after revocation (#516), stratum auth accept/reject + rotation, dashboard visibility/drop-off, dev-fee independence, and the sister-API **hashrate-impact guard** (#99 must not shave H/s under polling load). | `PITHEAD_URL=host:3333 sudo -E make e2e-pithead` (see [`RELEASING.md`](../RELEASING.md)) | +| **Release e2e (worker↔stack)** | [`e2e-pithead.sh`](e2e-pithead.sh) ([`control` phase](e2e-pithead-control.sh), [`access-tokens` phase](e2e-pithead-tokens.sh)) | Real rig + a **live Pithead stack**, **manual, root**. Not in CI (`connect`/`control`/`access-tokens` also run as a `rigforge`/`tier4-e2e` bench-ci job). | The integration contract: the `network` phase records fixed boolean summary diagnostics (`network-summary`) before enforcing metadata and the XMRig key superset, without logging response bodies or key names; `network-sequence` reproduces the nightly `connect` → `worker-api` → `api-impact` → `network` prefix in one process/snapshot and stops there (separate phase submissions restore between phases); mining round-trip against the stack, the `:8080` API posture (open/token/`restricted`), the writable control path's DONATION apply round trip (#344/#509), a named `ACCESS_TOKENS` entry authenticating `:8081`/`:8082` while staying refused on `:8080` and after revocation (#516), stratum auth accept/reject + rotation, dashboard visibility/drop-off, dev-fee independence, and the sister-API **hashrate-impact guard** (#99 must not shave H/s under polling load). | `PITHEAD_URL=host:3333 sudo -E make e2e-pithead` (see [`RELEASING.md`](../RELEASING.md)) | The unit + black-box suite, the Linux container e2e, and the coverage gate run automatically on every push/PR (see [`.github/workflows/ci.yml`](../.github/workflows/ci.yml)). The native macOS e2e is diff --git a/tests/e2e-pithead.sh b/tests/e2e-pithead.sh index e2eafd6..fbb5469 100644 --- a/tests/e2e-pithead.sh +++ b/tests/e2e-pithead.sh @@ -14,7 +14,7 @@ RIGFORGE="$HERE/rigforge.sh" CFG="$HERE/config.json" source "$(dirname "${BASH_SOURCE[0]}")/e2e-pithead-control.sh" && source "$(dirname "${BASH_SOURCE[0]}")/e2e-pithead-tokens.sh" # phase_control (#509), phase_access_tokens (#516) -source "$(dirname "${BASH_SOURCE[0]}")/e2e-control-replay.sh" +source "$(dirname "${BASH_SOURCE[0]}")/e2e-control-replay.sh" && source "$HERE/tests/e2e-summary-diagnostics.sh" # bench-ci-fixture: pithead-control control-replay-thermal control-replay-pools PASS=0 FAIL=0 E2E_EXIT_RC=0 ok() { @@ -370,16 +370,15 @@ phase_network() { else bad "miner has unexpected listeners: $xl" fi - # Require the documented metadata, then compare the remaining keys exactly with XMRig's API. k1=$(api8080 http://127.0.0.1:8080/2/summary | jq -cS 'keys' 2>/dev/null || true) k2=$(api8081 http://127.0.0.1:8081/2/summary) + printf '%s\n' "${k1:-null}" "${k2:-null}" | summary_contract_diagnostics if [ -n "$k1" ] && printf '%s' "$k2" | jq -e '(.rigforge | type) == "object" and (.generated_at | type) == "string" and (.generated_at as $g | [try ($g | fromdateiso8601 | strftime("%Y-%m-%dT%H:%M:%SZ")) catch empty] == [$g])' >/dev/null 2>&1 && [ "$k1" = "$(printf '%s' "$k2" | jq -cS 'del(.rigforge, .generated_at) | keys' 2>/dev/null || true)" ]; then ok "wire superset: sister /2/summary = xmrig's keys + rigforge + generated_at" else bad "sister summary lacks valid metadata or its XMRig key superset differs" fi - # No response on either port may expose the configured token or pool password. set_cfg '.ACCESS_TOKEN = "tok-net1" | .pools[0].pass = "pass-net1"' sleep 3 sweep=$( @@ -534,7 +533,7 @@ summary() { require_preflight "$@" # Validate the phase against its own `phase_` function: a typo dies with the rig untouched. # #183: the lock follows, before snapshot_config (whose _cleanup runs `apply`) and the first API touch. -[ "${1:-all}" = all ] || declare -F "phase_${1//-/_}" >/dev/null || die "unknown phase '$1' (connect|worker-api|api-impact|network|stratum-auth|dashboard|dev-fee|control|control-replay-thermal|control-replay-pools|access-tokens|all)" +[ "${1:-all}" = all ] || declare -F "phase_${1//-/_}" >/dev/null || die "unknown phase '$1' (connect|worker-api|api-impact|network|network-sequence|stratum-auth|dashboard|dev-fee|control|control-replay-thermal|control-replay-pools|access-tokens|all)" replay_lock "${1:-all}" snapshot_config @@ -543,6 +542,7 @@ connect) phase_connect ;; worker-api) phase_worker_api ;; api-impact) phase_api_impact ;; network) phase_network ;; +network-sequence) phase_network_sequence ;; stratum-auth) phase_stratum_auth ;; dashboard) phase_dashboard ;; dev-fee) phase_dev_fee ;; @@ -555,7 +555,7 @@ all) [ "$FAIL" -eq 0 ] || break done ;; -*) die "unknown phase '$1' (connect|worker-api|api-impact|network|stratum-auth|dashboard|dev-fee|control|control-replay-thermal|control-replay-pools|access-tokens|all)" ;; +*) die "unknown phase '$1' (connect|worker-api|api-impact|network|network-sequence|stratum-auth|dashboard|dev-fee|control|control-replay-thermal|control-replay-pools|access-tokens|all)" ;; esac _cleanup || bad "pre-test config/runtime restoration failed; snapshot retained at $SAVED_CFG" trap - EXIT diff --git a/tests/e2e-summary-diagnostics.sh b/tests/e2e-summary-diagnostics.sh new file mode 100644 index 0000000..469a1ca --- /dev/null +++ b/tests/e2e-summary-diagnostics.sh @@ -0,0 +1,28 @@ +# shellcheck shell=bash +# Fixed-schema observations only: never echo bodies, key names, timestamps or parser errors. +summary_contract_diagnostics() { + local result + result=$(jq -cs ' + if length != 2 then error("record count") else . end | + .[0] as $keys | .[1] as $body | + (if ($body | type) == "object" then $body else {} end) as $s | + {json_valid: true, + worker_keys: (($keys | type) == "array"), + sister_object: (($body | type) == "object"), + rigforge_object: (($s.rigforge | type) == "object"), + generated_at_valid: (try ($s.generated_at as $g | + ($g | type) == "string" and + [($g | fromdateiso8601 | strftime("%Y-%m-%dT%H:%M:%SZ"))] == [$g]) catch false), + xmrig_unreachable: (try ($s.rigforge.xmrig_api == "unreachable") catch false), + keys_match: ($keys == ($s | del(.rigforge, .generated_at) | keys))} + ' 2>/dev/null) || result='{"json_valid":false}' + printf 'network-summary: %s\n' "$result" +} +# Preserve the nightly prefix in one snapshot, without running unrelated later phases. +phase_network_sequence() { + local run_phase + for run_phase in phase_connect phase_worker_api phase_api_impact phase_network; do + "$run_phase" + [ "$FAIL" -eq 0 ] || return 1 + done +} diff --git a/tests/test-e2e-gate-fail-closed.sh b/tests/test-e2e-gate-fail-closed.sh index c78d8c6..4748591 100644 --- a/tests/test-e2e-gate-fail-closed.sh +++ b/tests/test-e2e-gate-fail-closed.sh @@ -2,6 +2,7 @@ # shellcheck disable=SC2016,SC2034,SC2329 # Hardware-free regression for the release-gate failure plumbing (#491). echo "== unit: release e2e gates fail closed (#491) ==" +source "$ROOT/tests/test-summary-diagnostics.sh" source "$ROOT/tests/e2e-refresh-window.sh" T491="$(mktemp -d "$SANDBOX/gate491.XXXXXX")" @@ -64,7 +65,7 @@ pit_dispatch_case() { # -> what-ran:rc out="$( eval "$PIT_DIE" FAIL=0 - for p in connect worker_api api_impact network stratum_auth dashboard dev_fee control access_tokens; do + for p in connect worker_api api_impact network network_sequence stratum_auth dashboard dev_fee control access_tokens; do eval "phase_$p() { printf '%s ' $p; }" done set -- "$1" @@ -73,6 +74,7 @@ pit_dispatch_case() { # -> what-ran:rc rc=$? printf '%s:%s\n' "${out:-nothing}" "$rc" } +assert_eq "dispatch selects the network sequence" "$(pit_dispatch_case network-sequence)" "network_sequence :0" assert_eq "dispatch runs the named phase" "$(pit_dispatch_case worker-api)" "worker_api :0" assert_eq "dispatch runs every phase for 'all'" "$(pit_dispatch_case all)" "connect worker_api api_impact network stratum_auth dashboard dev_fee control access_tokens :0" assert_eq "an underscored typo dies instead of silently passing with nothing run" "$(pit_dispatch_case worker_api)" "nothing:2" diff --git a/tests/test-summary-diagnostics.sh b/tests/test-summary-diagnostics.sh new file mode 100644 index 0000000..c8c1e59 --- /dev/null +++ b/tests/test-summary-diagnostics.sh @@ -0,0 +1,90 @@ +# shellcheck shell=bash +# shellcheck disable=SC2034 +# Hardware-free diagnosis of #563; raw response fields must never reach the job log. +source "$ROOT/tests/e2e-summary-diagnostics.sh" +summary_diag_case() { printf '%s\n' "$1" "$2" | summary_contract_diagnostics; } +DIAG_GOOD='{"hashrate":{},"rigforge":{},"generated_at":"2026-10-02T06:00:00Z"}' +diag=$(summary_diag_case '["hashrate"]' "$DIAG_GOOD") +assert_eq "summary diagnostics: valid superset classified" "$diag" 'network-summary: {"json_valid":true,"worker_keys":true,"sister_object":true,"rigforge_object":true,"generated_at_valid":true,"xmrig_unreachable":false,"keys_match":true}' +assert_eq "summary diagnostics: absent record sanitized" "$(summary_diag_case '["hashrate"]' '')" 'network-summary: {"json_valid":false}' +for payload in 'null' '[]' '"secret-marker"' 'true' '42'; do + diag=$(summary_diag_case '["hashrate"]' "$payload") + assert_contains "summary diagnostics: missing/non-object sister classified" "$diag" '"sister_object":false' +done +for stamp in '' 'secret-marker' '2026-10-02T06:00:00+00:00'; do + diag=$(summary_diag_case '["hashrate"]' "$(printf '%s' "$DIAG_GOOD" | jq --arg s "$stamp" '.generated_at=$s')") + assert_contains "summary diagnostics: invalid timestamp classified" "$diag" '"generated_at_valid":false' + assert_absent "summary diagnostics: timestamp value omitted" "$diag" 'secret-marker' +done +diag=$(summary_diag_case '["hashrate"]' '{"rigforge":{"xmrig_api":"unreachable","secret":"secret-marker"},"generated_at":"2026-10-02T06:00:00Z"}') +assert_contains "summary diagnostics: unreachable snapshot distinguished" "$diag" '"xmrig_unreachable":true' +assert_contains "summary diagnostics: missing XMRig keys distinguished" "$diag" '"keys_match":false' +assert_absent "summary diagnostics: nested secrets omitted" "$diag" 'secret-marker' +diag=$(summary_diag_case '["secret-marker"]' "$DIAG_GOOD") +assert_absent "summary diagnostics: key names omitted" "$diag" 'secret-marker' +assert_eq "summary diagnostics: malformed JSON sanitized" "$(summary_diag_case '["hashrate"]' '{secret-marker')" 'network-summary: {"json_valid":false}' +assert_eq "summary diagnostics: multiple records refused" "$(summary_diag_case '[]' '{} {}')" 'network-summary: {"json_valid":false}' +# Exercise phase wiring and keep the original failing assertion fatal. +( + eval "$(sed -n '/^phase_network()/,/^}/p' "$ROOT/tests/e2e-pithead.sh")" + HERE="$ROOT" PITHEAD_URL=pool.invalid:3333 + phase() { :; } + set_cfg() { :; } + sleep() { :; } + ss() { + case "$1" in + -Htnp) printf 'ESTAB 0 0 local pool.invalid:3333 xmrig\n' ;; + -Htln) printf 'LISTEN 0 0 *:8081 peer\n' ;; + -Htlnp) printf 'LISTEN 0 0 *:8080 peer xmrig\n' ;; + esac + } + api8080() { printf '{"hashrate":{}}'; } + api8081() { printf '{"rigforge":{"xmrig_api":"unreachable"}}'; } + ok() { :; } + bad() { + printf 'FAIL:%s\n' "$1" + return 1 + } + set -e + phase_network +) >"$SANDBOX/summary-diag-phase" 2>&1 +rc=$? +assert_rc "summary diagnostics: wire failure remains fatal" "$rc" 1 +assert_contains "summary diagnostics: phase emits observations before failure" "$(cat "$SANDBOX/summary-diag-phase")" 'network-summary:' +assert_contains "summary diagnostics: original failing row retained" "$(cat "$SANDBOX/summary-diag-phase")" 'FAIL:sister summary lacks valid metadata or its XMRig key superset differs' +# No inter-phase cleanup: the same shell state must reach the network check. +summary_sequence_case() { + ( + set -e + FAIL=0 sequence_state=initial + phase_connect() { + sequence_state=connected + printf 'connect ' + } + phase_worker_api() { + [ "$sequence_state" = connected ] + sequence_state=worker + printf 'worker-api ' + } + fault=$1 + phase_api_impact() { + [ "$sequence_state" = worker ] + sequence_state=loaded + printf 'api-impact ' + case "$fault" in fatal) return 7 ;; row) FAIL=1 ;; esac + return 0 + } + phase_network() { + [ "$sequence_state" = loaded ] + printf network + } + phase_network_sequence + ) +} +assert_eq "network sequence: ordered nightly prefix shares shell state" "$(summary_sequence_case success)" 'connect worker-api api-impact network' +sequence_out=$(summary_sequence_case fatal) +assert_rc "network sequence: fatal phase status propagates" "$?" 7 +assert_eq "network sequence: fatal predecessor stops before network" "$sequence_out" 'connect worker-api api-impact ' +sequence_out=$(summary_sequence_case row) +assert_rc "network sequence: failed row stops the prefix" "$?" 1 +assert_eq "network sequence: failed predecessor row stops before network" "$sequence_out" 'connect worker-api api-impact '