diff --git a/tests/README.md b/tests/README.md index 872c08a..ebc0dea 100644 --- a/tests/README.md +++ b/tests/README.md @@ -25,6 +25,30 @@ push/PR (see [`.github/workflows/ci.yml`](../.github/workflows/ci.yml)). The nat manual and local-only — macOS is deprecated (#493), so it never runs in CI. The last two are deliberately kept out of CI, because a real build, HugePages, and live mining are flaky by nature and against GitHub Actions' ToS. They're a manual pre-tag gate the releaser runs. +The queued `control` phase also instruments real timer-driven API refreshes around a thermal +control apply (#546). A temporary runtime drop-in replaces only the refresh service's command +with `e2e-api-refresh-profile.sh`; its priority, timeout, timer and live feed remain in use. +The profiler logs fixed helper names, begin/end timestamps and nanosecond durations, never +arguments or payloads. Five-second observations record timer/service state and the direct feed +for five minutes around the apply; the changed ceiling must still appear within 90 seconds. +Control-phase requests pass the temporary bearer token through curl's stdin configuration, +keeping it out of process arguments. Each POST uses its own securely allocated response file, +removed after reading the change identifier. +Cleanup quiesces the timer and instrumented pass, emits only measurement records from the journal, removes +the drop-in, reloads systemd and restores the timer’s prior active state before config/runtime restoration. +The job fails if journal collection or measurement export fails, no profiled refresh completes +successfully, or cleanup fails. Begin events from unfinished helpers are retained to locate stalled +operations, including a pass stopped by cleanup. +For affected-workload acceptance, use a reserved `rigforge` `tier4-e2e` job with +`phases=["connect","control"]`: connect proves mining before control on the same reserved worker. +A `phases=["control"]` run validates diagnostic collection only; +do not invoke the profiler against a rig yourself. Routing still chooses an eligible worker: +this captures the same thermal-control/service path, but does not guarantee the original fixture. +Short passes or an injected delay do not attribute the historical stale-feed failure; #540's +fix acceptance requires evidence of the same failure mechanism. Profiling adds clock-read overhead +and the source remains the submitted RigForge head; no alternative production implementation is +installed by this diagnostic. + Both stop at a failed mutation prerequisite and make restoration failure fail the process; recovery snapshots remain on disk when cleanup cannot prove the original config bytes and runtime were restored. diff --git a/tests/e2e-api-refresh-profile.sh b/tests/e2e-api-refresh-profile.sh new file mode 100644 index 0000000..204d68d --- /dev/null +++ b/tests/e2e-api-refresh-profile.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# #546: executed by the reserved harness's refresh-service override, never standalone on a rig. +# Reuses #540's fixed-helper wrappers, now inside real timer dispatches rather than a scratch A/B. +set -euo pipefail +# shellcheck disable=SC1090 +source "$1" +refresh_profile_event() { + printf 'refresh-profile: pid=%s helper=%s event=%s at_ns=%s duration_ns=%s rc=%s\n' \ + "$$" "$1" "$2" "$3" "$4" "$5" >&2 +} +# No arguments, config values or payloads in diagnostics. Begin events identify an unfinished probe. +for probe in api_refresh parse_config _read_api_summary _api_rigforge_block _api_tune_json _api_power_json _health_json _cpu_eff_khz _mem_summary _msr_log_status _dmi _watchdog_json _api_config_json _api_config_meta_json _api_control_json _api_control_history_json; do + declare -f "$probe" >/dev/null + eval "$(declare -f "$probe" | sed "1s/$probe/${probe}_profile_original/")" + eval "$probe() { + local profile_start profile_end profile_rc + profile_start=\$(date +%s%N) + refresh_profile_event $probe begin \"\$profile_start\" 0 0 + ${probe}_profile_original \"\$@\" + profile_rc=\$? + profile_end=\$(date +%s%N) + refresh_profile_event $probe end \"\$profile_end\" \"\$((profile_end - profile_start))\" \"\$profile_rc\" + return \"\$profile_rc\" + }" +done +api_refresh diff --git a/tests/e2e-pithead-control.sh b/tests/e2e-pithead-control.sh index f941bb6..0547d64 100644 --- a/tests/e2e-pithead-control.sh +++ b/tests/e2e-pithead-control.sh @@ -3,23 +3,36 @@ # (docs/dev/file-budget.tsv) — same convention as tests/run.sh sourcing its own test-e2e-*.sh # fragments. Sourced by e2e-pithead.sh AFTER phase()/ok()/bad()/set_cfg()/$CFG are defined; this # file declares no preflight of its own and must not be run standalone. +source "$(dirname "${BASH_SOURCE[0]}")/e2e-refresh-window.sh" + +_control_curl() { # Keep the temporary write token out of process arguments. + local token="$1" + shift + printf 'header = %s\n' "$(printf 'Authorization: Bearer %s' "$token" | jq -Rs .)" | curl --config - "$@" +} + phase_control() { phase "control — writable control path DONATION apply round trip (#344/#509)" - local tok cur new port resp code cid st waited=0 body + local tok cur new port api_port resp code cid st waited=0 body max_temp target_temp tok=$(head -c 32 /dev/urandom | xxd -p -c 256) cur=$(jq -r '.DONATION // 1' "$CFG") new=$(((cur + 1) % 101)) - set_cfg ".api=\"enabled\" | .control=\"enabled\" | .ACCESS_TOKEN=\"$tok\" | .api_allow_from=\"127.0.0.1/32\"" soft || bad "could not enable the control path" + max_temp=$(jq -r 'if .max_temp_c == null or .max_temp_c == "" then 100 else .max_temp_c end' "$CFG") + [ "$max_temp" -le 100 ] || max_temp=100 + [ "$max_temp" -gt 40 ] || bad "cannot lower the existing thermal cutoff safely" + target_temp=$((max_temp - 1)) + set_cfg ".api=\"enabled\" | .control=\"enabled\" | .ACCESS_TOKEN=\"$tok\" | .api_allow_from=\"127.0.0.1/32\" | .watchdog=\"enabled\" | .max_temp_c=$max_temp" soft || bad "could not enable the control path" sleep 3 # let rigforge-control.service (restarted by install_control) settle port=$(jq -r '.control_port // 8082' "$CFG") + api_port=$(jq -r '.api_port // 8081' "$CFG") resp="$(mktemp)" - code=$(curl -s -o "$resp" -w '%{http_code}' --max-time 10 -H "Authorization: Bearer $tok" \ + code=$(_control_curl "$tok" -s -o "$resp" -w '%{http_code}' --max-time 10 \ -H "Content-Type: application/json" -d "{\"DONATION\": $new}" "http://127.0.0.1:$port/apply" 2>/dev/null || true) cid=$(jq -r '.change_id // empty' "$resp" 2>/dev/null || true) rm -f "$resp" [ "$code" = 202 ] && [ -n "$cid" ] && ok "POST /apply accepted (change_id=$cid)" || bad "POST /apply returned HTTP '$code' (expected 202)" while [ "$waited" -lt 300 ]; do - body=$(curl -fsS --max-time 5 -H "Authorization: Bearer $tok" "http://127.0.0.1:$port/status?change_id=$cid" 2>/dev/null || true) + body=$(_control_curl "$tok" -fsS --max-time 5 "http://127.0.0.1:$port/status?change_id=$cid" 2>/dev/null || true) st=$(printf '%s' "$body" | jq -r '.status // empty' 2>/dev/null || true) case "$st" in applied | rejected | rolled_back | failed) break ;; esac sleep 5 @@ -31,6 +44,35 @@ phase_control() { [ "$(jq -r '.DONATION' "$CFG")" = "$new" ] && ok "config.json carries DONATION=$new (control-apply persisted it)" || bad "config.json DONATION is '$(jq -r '.DONATION' "$CFG")', expected $new" + refresh_profile_start || bad "could not instrument timer-driven refreshes" + sleep 20 # allow a natural timer dispatch before thermal apply + local before feed_temp feed_stamp started observed=0 + before=$(date -u +%Y-%m-%dT%H:%M:%SZ) + resp="$(mktemp)" || bad "could not allocate the thermal response file" + code=$(_control_curl "$tok" -s -o "$resp" -w '%{http_code}' --max-time 10 \ + -H "Content-Type: application/json" -d "{\"max_temp_c\":$target_temp}" "http://127.0.0.1:$port/apply" 2>/dev/null || true) + cid=$(jq -r '.change_id // empty' "$resp" 2>/dev/null || true) + rm -f "$resp" + [ "$code" = 202 ] && [ -n "$cid" ] || bad "max_temp_c apply was not accepted (HTTP $code)" + started=$SECONDS + while [ "$((SECONDS - started))" -lt 300 ]; do + refresh_profile_state || bad "miner or refresh-state observation failed" + body=$(_control_curl "$tok" -fsS --max-time 5 "http://127.0.0.1:$port/status?change_id=$cid" 2>/dev/null || true) + st=$(printf '%s' "$body" | jq -r '.status // empty' 2>/dev/null || true) + body=$(_control_curl "$tok" -fsS --max-time 5 "http://127.0.0.1:$api_port/1/summary" 2>/dev/null || true) + feed_temp=$(printf '%s' "$body" | jq -r '.rigforge.watchdog.max_temp_c // empty' 2>/dev/null || true) + feed_stamp=$(printf '%s' "$body" | jq -r '.generated_at // empty' 2>/dev/null || true) + printf 'refresh-window: elapsed_s=%s status=%s max_temp_c=%s generated_at=%s\n' "$((SECONDS - started))" "${st:-unknown}" "${feed_temp:-missing}" "${feed_stamp:-missing}" + if [ "$((SECONDS - started))" -lt 90 ] && [ "$st" = applied ] && [ "$feed_temp" = "$target_temp" ] && [[ "$feed_stamp" > "$before" ]] && [ "$observed" = 0 ]; then + observed=$((SECONDS - started + 1)) + fi + sleep 5 + done + refresh_profile_finish || bad "timer profiling or restoration failed" + waited=$((observed - 1)) + [ "$observed" -gt 0 ] && + ok "applied max_temp_c reaches a new direct sister-API summary within ${waited}s (#540)" || + bad "max_temp_c feed stale after 90s (status=${st:-unknown}, temp=${feed_temp:-missing}, generated_at=${feed_stamp:-missing})" # No token blanking here: _cleanup's EXIT trap restores DONATION, the token and the control # state from the snapshot, and blanking ACCESS_TOKEN while control is enabled is #514's own # fail-closed abort (rigforge.sh refuses that apply) — the bug this phase must not re-enact. diff --git a/tests/e2e-pithead.sh b/tests/e2e-pithead.sh index a1c447e..6752ce6 100644 --- a/tests/e2e-pithead.sh +++ b/tests/e2e-pithead.sh @@ -106,6 +106,7 @@ _restore_xmrig() { } _cleanup() { local cleanup_ok=1 p + refresh_profile_finish || cleanup_ok=0 for p in $HAMMER_PIDS; do kill "$p" 2>/dev/null || true; done if [ -n "$SAVED_CFG" ] && [ -f "$SAVED_CFG" ] && cp "$SAVED_CFG" "$CFG" && "$RIGFORGE" apply >/dev/null 2>&1 && cmp -s "$SAVED_CFG" "$CFG"; then @@ -168,8 +169,6 @@ wait_for_job() { # -> 0 when the log shows a stratum job return 1 } -# --- phases --- - phase_connect() { phase "connect — worker mines against the live stack ($PITHEAD_URL)" set_cfg ".pools[0].url = \"$PITHEAD_URL\"" diff --git a/tests/e2e-refresh-window.sh b/tests/e2e-refresh-window.sh new file mode 100644 index 0000000..f05a4e2 --- /dev/null +++ b/tests/e2e-refresh-window.sh @@ -0,0 +1,58 @@ +# shellcheck shell=bash +# Reserved control-phase lifecycle; overrides only ExecStart, retaining the real service policy. +REFRESH_PROFILE_DROPIN="" REFRESH_PROFILE_SINCE="" REFRESH_PROFILE_TIMER_ACTIVE="" +refresh_profile_start() { + local dir="${REFRESH_PROFILE_UNIT_ROOT:-/run/systemd/system}/rigforge-api-refresh.service.d" + local script + script="$(dirname "$RIGFORGE")/tests/e2e-api-refresh-profile.sh" + # Refuse collisions and paths requiring systemd quoting rather than generating executable text. + case "$script$RIGFORGE" in *[!a-zA-Z0-9_./-]*) return 1 ;; esac + [ ! -L "$dir" ] && [ ! -e "$dir/rigforge-profile.conf" ] && [ ! -L "$dir/rigforge-profile.conf" ] || return 1 + mkdir -p "$dir" || return 1 + REFRESH_PROFILE_TIMER_ACTIVE=0 + systemctl is-active --quiet rigforge-api-refresh.timer && REFRESH_PROFILE_TIMER_ACTIVE=1 + REFRESH_PROFILE_SINCE=$(date -u '+%Y-%m-%d %H:%M:%S') + local dropin="$dir/rigforge-profile.conf" + ( + umask 077 + set -o noclobber + printf '[Service]\nExecStart=\nExecStart=/bin/bash %s %s\n' "$script" "$RIGFORGE" >"$dropin" + ) || return 1 + REFRESH_PROFILE_DROPIN="$dropin" + systemctl daemon-reload +} +refresh_profile_state() { + printf 'refresh-window: state at_ns=%s\n' "$(date +%s%N)" + systemctl is-active --quiet xmrig || return 1 + systemctl show rigforge-api-refresh.timer rigforge-api-refresh.service \ + -p ActiveState -p SubState -p Result -p LastTriggerUSec -p NextElapseUSecRealtime \ + -p ExecMainStartTimestamp -p ExecMainExitTimestamp -p ExecMainStatus +} +refresh_profile_finish() { + [ -n "$REFRESH_PROFILE_DROPIN" ] || return 0 + local log rc=0 + refresh_profile_state || rc=1 + # Quiesce both units before collecting: the timer could otherwise dispatch during cleanup. + systemctl stop rigforge-api-refresh.timer || rc=1 + systemctl stop rigforge-api-refresh.service || rc=1 + log=$(mktemp) || rc=1 + if [ -n "$log" ]; then + journalctl --sync || rc=1 + journalctl -u rigforge-api-refresh.service --since "$REFRESH_PROFILE_SINCE UTC" --no-pager -o cat >"$log" || rc=1 + # Publish only the fixed-schema measurements; other unit journal messages may contain secrets. + sed -nE '/^refresh-profile: pid=[0-9][0-9]* helper=[a-z_][a-z_]* event=(begin|end) at_ns=[0-9][0-9]* duration_ns=[0-9][0-9]* rc=[0-9][0-9]*$/p' "$log" || rc=1 + grep -Eq '^refresh-profile: .*helper=api_refresh event=end .*rc=0$' "$log" || rc=1 + rm -f "$log" + fi + if rm -f "$REFRESH_PROFILE_DROPIN" && systemctl daemon-reload; then + if [ "$REFRESH_PROFILE_TIMER_ACTIVE" = 0 ] || systemctl start rigforge-api-refresh.timer; then + REFRESH_PROFILE_DROPIN="" + REFRESH_PROFILE_TIMER_ACTIVE="" + else + rc=1 + fi + else + rc=1 + fi + return "$rc" +} diff --git a/tests/test-api-refresh-liveness.sh b/tests/test-api-refresh-liveness.sh index 7968283..b22ab44 100644 --- a/tests/test-api-refresh-liveness.sh +++ b/tests/test-api-refresh-liveness.sh @@ -251,3 +251,9 @@ assert_eq "watchdog cleanup fails when prior active state cannot be restored (#4 # subprocess and assert it stays silent. Red before the `|| true` guard: two on_err blocks on stderr. APIERR="$(mktemp -d "$SANDBOX/apierr.XXXXXX")" assert_eq "api-refresh: no control changes/ ring does not trip errexit or the ERR trap (#519)" "$(RIGFORGE_HOME="$APIQ" RIGFORGE_API_DATA="$APIERR" RIGFORGE_CONTROL_STATE="$APIERR/ctl" PATH="$STUBS:$PATH" bash "$SCRIPT" api-refresh 2>&1 >/dev/null)" "" + +# Harness diagnostics have their own hardware-free regression suite (#546). +bash "$ROOT/tests/test-refresh-profile.sh" +assert_rc "timer-refresh diagnostics regression suite (#546)" "$?" "0" +bash "$ROOT/tests/test-refresh-window-control.sh" +assert_rc "thermal refresh-window regression suite (#546)" "$?" "0" diff --git a/tests/test-e2e-gate-fail-closed.sh b/tests/test-e2e-gate-fail-closed.sh index 558989d..c78d8c6 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/e2e-refresh-window.sh" T491="$(mktemp -d "$SANDBOX/gate491.XXXXXX")" PIT_SET="$(sed -n '/^set_cfg()/,/^}/p' "$ROOT/tests/e2e-pithead.sh")" diff --git a/tests/test-refresh-profile.sh b/tests/test-refresh-profile.sh new file mode 100644 index 0000000..2be9763 --- /dev/null +++ b/tests/test-refresh-profile.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Hardware-free checks of #546's timing, output boundary and runtime override lifecycle. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CASE_DIR=$(mktemp -d) +trap 'rm -rf "$CASE_DIR"' EXIT +check() { if ! "$@"; then + printf 'refresh-profile regression failed: %s\n' "$*" >&2 + exit 1 +fi; } +export TEST_PAYLOAD="$CASE_DIR/payload" +cat >"$CASE_DIR/fixture.sh" <"\$TEST_PAYLOAD"; } +EOF_FIXTURE +bash "$ROOT/tests/e2e-api-refresh-profile.sh" "$CASE_DIR/fixture.sh" 2>"$CASE_DIR/measurements" +check grep -q '^refresh-profile: .*helper=_health_json event=begin ' "$CASE_DIR/measurements" +check grep -q '^refresh-profile: .*helper=api_refresh event=end .*rc=0$' "$CASE_DIR/measurements" +check test "$(cat "$TEST_PAYLOAD")" = private-probe-value +check bash -c '! grep -q private-probe-value "$1"' -- "$CASE_DIR/measurements" +# A 50ms probe must be visible below one second, rather than rounded to zero as in job 1876. +duration=$(sed -n 's/.*helper=_health_json event=end .*duration_ns=\([0-9]*\) rc=0/\1/p' "$CASE_DIR/measurements") +check test "$duration" -ge 1000000 +check test "$((duration % 1000000000))" -ne 0 +if TEST_PROBE_RC=7 bash "$ROOT/tests/e2e-api-refresh-profile.sh" "$CASE_DIR/fixture.sh" 2>"$CASE_DIR/failed"; then + echo 'failed helper unexpectedly succeeded' >&2 + exit 1 +else + check test "$?" = 7 +fi +check grep -q 'helper=_health_json event=begin' "$CASE_DIR/failed" +check bash -c '! grep -q "helper=api_refresh event=end" "$1"' -- "$CASE_DIR/failed" +# Stubs below simulate systemd/journal; never issue commands against host units. +export REFRESH_PROFILE_UNIT_ROOT="$CASE_DIR/units" +RIGFORGE="$ROOT/rigforge.sh" +source "$ROOT/tests/e2e-refresh-window.sh" +systemctl() { printf '%s\n' "$*" >>"$CASE_DIR/calls"; } +journalctl() { + [ "$1" != --sync ] || return 0 + cat "$CASE_DIR/measurements" + printf 'sensitive unit output must not escape\n' +} +refresh_profile_start +owned="$REFRESH_PROFILE_DROPIN" +check grep -q '^ExecStart=/bin/bash ' "$owned" +check bash -c '! grep -Eq "^(Nice|IOSchedulingClass|TimeoutStartSec)=" "$1"' -- "$owned" +check bash -c '! grep -q "start rigforge-api-refresh.service" "$1"' -- "$CASE_DIR/calls" +refresh_profile_finish >"$CASE_DIR/collected" +check test ! -e "$owned" +check test -z "$REFRESH_PROFILE_DROPIN" +check grep -q 'stop rigforge-api-refresh.service' "$CASE_DIR/calls" +check grep -q 'helper=_health_json event=end' "$CASE_DIR/collected" +check bash -c '! grep -q sensitive "$1"' -- "$CASE_DIR/collected" +# Timer must be quiesced before the service; restore it only after override removal/reload. +check awk ' + $0 == "stop rigforge-api-refresh.timer" { a = NR } + $0 == "stop rigforge-api-refresh.service" { b = NR } + $0 == "daemon-reload" && b { c = NR } + $0 == "start rigforge-api-refresh.timer" { d = NR } + END { exit !(a && a < b && b < c && c < d) } +' "$CASE_DIR/calls" +# Export failure must fail conditional callers while still restoring both units. +: >"$CASE_DIR/calls" +refresh_profile_start +sed() { return 7; } +if refresh_profile_finish >"$CASE_DIR/export-failed"; then + printf 'failed exporter unexpectedly reported successful diagnostics\n' >&2 + exit 1 +fi +unset -f sed +check test ! -e "$owned" +check test -z "$REFRESH_PROFILE_DROPIN" +check grep -q '^daemon-reload$' "$CASE_DIR/calls" +check grep -q '^start rigforge-api-refresh.timer$' "$CASE_DIR/calls" +# A timer that fails during the window must still restore its original active state. +refresh_profile_start +systemctl() { + printf '%s\n' "$*" >>"$CASE_DIR/failed-timer-calls" + [ "$*" != 'is-active --quiet rigforge-api-refresh.timer' ] +} +refresh_profile_finish >"$CASE_DIR/failed-timer-collected" +check grep -q '^start rigforge-api-refresh.timer$' "$CASE_DIR/failed-timer-calls" +systemctl() { printf '%s\n' "$*" >>"$CASE_DIR/calls"; } +# Preserve an already-inactive timer; do not spuriously start it. +systemctl() { + printf '%s\n' "$*" >>"$CASE_DIR/inactive-calls" + [ "$*" != 'is-active --quiet rigforge-api-refresh.timer' ] +} +refresh_profile_start +refresh_profile_finish >"$CASE_DIR/inactive-collected" +check bash -c '! grep -q "start rigforge-api-refresh.timer" "$1"' -- "$CASE_DIR/inactive-calls" +systemctl() { printf '%s\n' "$*" >>"$CASE_DIR/calls"; } +# Refuse an existing override without claiming it for cleanup. +printf 'existing override\n' >"$owned" +if refresh_profile_start; then exit 1; fi +refresh_profile_finish +check test "$(cat "$owned")" = 'existing override' +rm "$owned" +# A window without a completed profiled refresh cannot report successful diagnostics. +refresh_profile_start +journalctl() { :; } +if refresh_profile_finish >"$CASE_DIR/missing"; then exit 1; fi +check test ! -e "$owned" +# Even when journal collection fails, remove our override and propagate failure. +refresh_profile_start +journalctl() { return 1; } +if refresh_profile_finish >"$CASE_DIR/journal-failed"; then exit 1; fi +check test ! -e "$owned" +# Allocation failures must not strand a root ExecStart override. +refresh_profile_start +mktemp() { return 1; } +if refresh_profile_finish >"$CASE_DIR/allocation-failed"; then exit 1; fi +unset -f mktemp +check test ! -e "$owned" +# A failed stop must fail diagnostics while still attempting override/timer restoration. +refresh_profile_start +systemctl() { [ "$*" != 'stop rigforge-api-refresh.service' ]; } +if refresh_profile_finish >"$CASE_DIR/stop-failed"; then exit 1; fi +check test ! -e "$owned" +# The dispatch/cleanup are wired into the existing reserved harness. +check grep -q 'refresh_profile_start' "$ROOT/tests/e2e-pithead-control.sh" +check grep -q 'refresh_profile_finish || cleanup_ok=0' "$ROOT/tests/e2e-pithead.sh" +printf 'timer-refresh profiling regressions: PASS\n' diff --git a/tests/test-refresh-window-control.sh b/tests/test-refresh-window-control.sh new file mode 100644 index 0000000..14bdbb6 --- /dev/null +++ b/tests/test-refresh-window-control.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# #546: execute the control phase with fake APIs, units and time, never against the host. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +if [ "$#" = 0 ]; then + TEST_WINDOW_DIR=$(mktemp -d) + export TEST_WINDOW_DIR + trap 'rm -rf "$TEST_WINDOW_DIR"' EXIT + bash "$0" fresh >"$TEST_WINDOW_DIR/fresh.log" 2>&1 + grep -q 'new direct sister-API summary within' "$TEST_WINDOW_DIR/fresh.log" + grep -q '^finish-window elapsed=30[0-9]' "$TEST_WINDOW_DIR/fresh.log" + grep -q '^POST thermal=99$' "$TEST_WINDOW_DIR/fresh-posts" + grep -q '^profile started$' "$TEST_WINDOW_DIR/fresh.log" + bash "$0" substituted >"$TEST_WINDOW_DIR/substituted.log" 2>&1 + grep -q '^substituted response pathname preserved sentinel$' "$TEST_WINDOW_DIR/substituted.log" + if bash "$0" allocation-failed >"$TEST_WINDOW_DIR/allocation-failed.log" 2>&1; then + printf 'failed thermal response allocation unexpectedly succeeded\n' >&2 + exit 1 + fi + grep -q 'could not allocate the thermal response file' "$TEST_WINDOW_DIR/allocation-failed.log" + [ ! -e "$TEST_WINDOW_DIR/allocation-failed-posts" ] + for mode in stale late oldstamp rejected; do + if bash "$0" "$mode" >"$TEST_WINDOW_DIR/$mode.log" 2>&1; then + printf '%s unexpectedly passed freshness assertion\n' "$mode" >&2 + exit 1 + fi + if [ "$mode" != rejected ]; then + grep -q 'feed stale after 90s' "$TEST_WINDOW_DIR/$mode.log" + grep -q '^finish-window elapsed=30[0-9]' "$TEST_WINDOW_DIR/$mode.log" + fi + done + printf 'thermal refresh-window regressions: PASS\n' + exit 0 +fi +mode="$1" +CFG="$TEST_WINDOW_DIR/$mode.json" +# shellcheck disable=SC2034 # consumed by the sourced phase +RIGFORGE="$ROOT/rigforge.sh" +started=0 # the sourced phase shadows this clock origin in its dynamic scope +printf '{"DONATION":1,"max_temp_c":100,"control_port":8082,"api_port":8081}' >"$CFG" +source "$ROOT/tests/e2e-pithead-control.sh" +phase() { :; } +ok() { printf '%s\n' "$1"; } +bad() { + printf '%s\n' "$1" >&2 + exit 1 +} +set_cfg() { :; } +xxd() { + cat >/dev/null + printf '%064d\n' 0 +} +sleep() { SECONDS=$((SECONDS + $1)); } +date() { printf '2026-09-30T00:00:00Z\n'; } +refresh_profile_start() { printf 'profile started\n'; } +refresh_profile_state() { :; } +refresh_profile_finish() { printf 'finish-window elapsed=%s\n' "$((SECONDS - started))"; } +curl() { + [[ "$*" != *Bearer* ]] || return 1 + [ "$1 $2" = "--config -" ] || return 1 + grep -Eq '^header = "Authorization: Bearer [0-9a-f]{64}"$' || return 1 + local output="" data="" url="" arg temp=100 stamp='2026-09-30T00:00:00Z' + while [ "$#" -gt 0 ]; do + arg="$1" + shift + case "$arg" in + -o) + output="$1" + shift + ;; + -d) + data="$1" + shift + ;; + http://*) url="$arg" ;; + esac + done + if [ -n "$output" ]; then + if [[ "$data" == *DONATION* ]]; then + jq '.DONATION=2' "$CFG" >"$CFG.tmp" + mv "$CFG.tmp" "$CFG" + else + printf 'POST thermal=%s\n' "$(printf '%s' "$data" | jq -r '.max_temp_c')" >>"$TEST_WINDOW_DIR/$mode-posts" + if [ "$mode" = rejected ]; then + printf '{}' >"$output" + printf 403 + return 0 + fi + fi + printf '{"change_id":"fixture"}' >"$output" + if [ "$mode" = substituted ] && [[ "$data" != *DONATION* ]]; then + LC_ALL=C ls -ld "$output" | cut -c1-10 >"$TEST_WINDOW_DIR/$mode-response-mode" + fi + printf 202 + elif [[ "$url" == */status* ]]; then + printf '{"status":"applied"}' + else + if [ "$mode" != stale ] && { [ "$mode" != late ] || [ "$((SECONDS - started))" -ge 95 ]; }; then + temp=99 + [ "$mode" = oldstamp ] || stamp='2026-09-30T00:00:01Z' + fi + printf '{"generated_at":"%s","rigforge":{"watchdog":{"max_temp_c":%s}}}' "$stamp" "$temp" + fi +} +PLANTED_RESPONSE="" +if [ "$mode" = substituted ]; then + SENTINEL="$TEST_WINDOW_DIR/sentinel" + printf 'preserve original bytes\n' >"$SENTINEL" + rm() { + command rm "$@" + if [ "$*" = "-f $resp" ] && [ -z "$PLANTED_RESPONSE" ]; then + ln -s "$SENTINEL" "$resp" + PLANTED_RESPONSE="$resp" + fi + } +elif [ "$mode" = allocation-failed ]; then + mktemp() { + [ ! -e "$TEST_WINDOW_DIR/$mode-allocated" ] || return 1 + : >"$TEST_WINDOW_DIR/$mode-allocated" + command mktemp "$@" + } +fi +phase_control +if [ "$mode" = substituted ]; then + [ -n "$PLANTED_RESPONSE" ] || bad "response substitution fixture did not run" + command rm -f "$PLANTED_RESPONSE" + [ "$(cat "$SENTINEL")" = 'preserve original bytes' ] || bad "substituted response pathname overwrote sentinel" + [ "$(cat "$TEST_WINDOW_DIR/$mode-response-mode")" = '-rw-------' ] || bad "thermal response file was not secured" + printf 'substituted response pathname preserved sentinel\n' +fi