Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
26 changes: 26 additions & 0 deletions tests/e2e-api-refresh-profile.sh
Original file line number Diff line number Diff line change
@@ -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
50 changes: 46 additions & 4 deletions tests/e2e-pithead-control.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions tests/e2e-pithead.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,8 +169,6 @@ wait_for_job() { # <timeout_s> -> 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\""
Expand Down
58 changes: 58 additions & 0 deletions tests/e2e-refresh-window.sh
Original file line number Diff line number Diff line change
@@ -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"
}
6 changes: 6 additions & 0 deletions tests/test-api-refresh-liveness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions tests/test-e2e-gate-fail-closed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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")"
Expand Down
125 changes: 125 additions & 0 deletions tests/test-refresh-profile.sh
Original file line number Diff line number Diff line change
@@ -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" <<EOF_FIXTURE
source "$ROOT/rigforge.sh"
parse_config() { :; }
_health_json() { sleep 0.05; printf 'private-probe-value'; return "\${TEST_PROBE_RC:-0}"; }
api_refresh() { parse_config; _health_json >"\$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'
Loading