Skip to content
63 changes: 63 additions & 0 deletions tests/test-hup-parser.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -euo pipefail
# Inert parser controls only; no production code, network or real credentials.
unset GH_TOKEN GITHUB_TOKEN SSH_PRIVATE_KEY_B64
root=$(mktemp -d)
cleanup() { rm -rf -- "$root"; }
trap cleanup EXIT
command -v timeout >/dev/null
printf "DIAG: parser controls bash=%s\n" "$BASH_VERSION"
cat > "$root/child.sh" <<\CHILD
#!/usr/bin/env bash
set -euo pipefail
handler() { : > "$ready.handled"; exit 129; }
mode=$1
ready=$2
trap handler HUP
: > "$ready"
end=$((SECONDS + 2))
case "$mode" in
plain) while (( SECONDS < end )); do :; done ;;
command) while (( SECONDS < end )); do value=$(printf x); [[ $value == x ]]; done ;;
snapshot) while (( SECONDS < end )); do active_jobs=$(jobs -pr); while IFS= read -r value; do :; done <<< "$active_jobs"; done ;;
process) while (( SECONDS < end )); do while IFS= read -r value; do :; done < <(jobs -pr); done ;;
*) exit 90 ;;
esac
# Reaching this point means no HUP was handled; never count it as success.
exit 91
CHILD
cat > "$root/controller.sh" <<\CONTROL
#!/usr/bin/env bash
set -euo pipefail
root=$1 mode=$2 round=$3 delay=$4
ready="$root/ready-$mode-$round"
# Signal only the short-lived direct fixture child after its readiness marker.
env --default-signal=HUP,INT,TERM bash "$root/child.sh" "$mode" "$ready" &
child=$!
end=$((SECONDS + 2))
while [[ ! -f $ready ]] && (( SECONDS < end )); do sleep 0.001; done
if [[ ! -f $ready ]]; then
wait "$child" || true
exit 92
fi
sleep "$delay"
kill -HUP "$child"
status=0
wait "$child" || status=$?
printf "DIAG: mode=%s round=%s delay=%s exit=%s\n" "$mode" "$round" "$delay" "$status"
[[ $status == 129 && -f $ready.handled ]]
CONTROL
# 3 modes x 6 cases, at most 5 seconds per case (including forced teardown).
# Do not retry failures. Delay varies delivery, not a claim of exact parser timing.
# Legacy process mode already reproduced EOF in run 34226553908.
# Keep it available explicitly; the default gate validates the replacement.
modes="plain command snapshot"
if [[ ${HUP_PARSER_INCLUDE_LEGACY:-false} == true ]]; then modes="$modes process"; fi
for mode in $modes; do
round=0
for delay in 0 0.001 0.005 0.01 0.02 0.05; do
round=$((round + 1))
timeout --signal=TERM --kill-after=1s 4s bash "$root/controller.sh" "$root" "$mode" "$round" "$delay"
done
done
printf "PASS: all selected bounded HUP parser controls\n"
25 changes: 24 additions & 1 deletion tests/test-push-worker-registration.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
# Safe runtime identity only: no environment dump, credentials, or xtrace.
printf "DIAG: bash=%s kernel=%s\n" "$BASH_VERSION" "$(uname -r)"
dpkg-query -W bash libc6 2>/dev/null || true

ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
readonly ROOT_DIR
Expand Down Expand Up @@ -82,6 +85,19 @@ printf source-safe
assert_eq source-safe "$source_result" "source keeps zero runtime side effects"
# shellcheck source=../tools/push.sh
source "$SCRIPT"
(
# Inert direct-child job table: no SSH or production services.
sleep 10 & snapshot_child=$!
cleanup_snapshot_child() { kill -TERM "$snapshot_child" 2>/dev/null || true; wait "$snapshot_child" 2>/dev/null || true; }
trap cleanup_snapshot_child EXIT
job_is_active "$snapshot_child" || fail "snapshot misses active direct child"
if job_is_active 0; then fail "snapshot accepts absent PID"; fi
kill -TERM "$snapshot_child"
wait "$snapshot_child" 2>/dev/null || true
if job_is_active "$snapshot_child"; then fail "snapshot retains reaped child"; fi
trap - EXIT
pass "job snapshot preserves direct-child matching and reap semantics"
)
trap 'rm -rf "$TEST_DIR"' EXIT

setup_fixture() {
Expand Down Expand Up @@ -1435,7 +1451,7 @@ run_state_publication_signal_case() (
}
trap cleanup_publication_fixture EXIT
mode=active; [[ "$state" == cleanup_failed ]] && mode=cleanup
root="$TEST_DIR/state-publish-$state-$phase-$signal_name"; CURRENT_FIXTURE_ROOT=$root
root="$TEST_DIR/state-publish-$state-$phase-$signal_name${5:-}"; CURRENT_FIXTURE_ROOT=$root
write_active_grace_fixture "$root"
mkdir -m 0700 "$root/capture"
marker="$root/capture/publish-marker"
Expand All @@ -1453,6 +1469,8 @@ run_state_publication_signal_case() (
[[ "$worker_pid" =~ ^[1-9][0-9]*$ && "$worker_start" =~ ^[1-9][0-9]*$ && "$managed_sid" =~ ^[1-9][0-9]*$ ]] || fail "$state/$phase/$signal_name marker identity malformed"
wait_test_process_identity_present "$worker_pid" "$worker_start" || { cat "$marker" >&2; fail "$state/$phase/$signal_name worker identity missing at hook"; }
test_watchdog_process 30 "$root/watchdog-timeout" "$main_pid" & watchdog=$!; watchdog_start=$(wait_test_process_start "$watchdog")
printf "DIAG: publication state=%s phase=%s signal=%s main=%s/%s worker=%s/%s sid=%s\n" \
"$state" "$phase" "$signal_name" "$main_pid" "$main_start" "$worker_pid" "$worker_start" "$managed_sid"
kill "-$signal_name" "$main_pid"; wait "$main_pid" || rc=$?
kill -TERM "$watchdog" 2>/dev/null || true; wait "$watchdog" 2>/dev/null || true
[[ ! -e "$root/watchdog-timeout" ]] || fail "$state/$phase/$signal_name watchdog fired"
Expand Down Expand Up @@ -1490,6 +1508,11 @@ for publication_state in $publication_states; do
done

(
# Three bounded extra probes; preserve original matrix and stop at first failure.
for diagnostic_round in 1 2 3; do
printf "DIAG: extra HUP probe=%s/3\n" "$diagnostic_round"
run_state_publication_signal_case cleanup_failed after-rename HUP 129 "-probe-$diagnostic_round"
done
root="$TEST_DIR/normal-parallel"; setup_fixture "$root"
: > "$root/capture/current"; : > "$root/capture/max"
push_to_server() {
Expand Down
7 changes: 5 additions & 2 deletions tools/push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -941,10 +941,13 @@ process_identity_matches() {
}

job_is_active() {
local expected="$1" pid
local expected="$1" pid active_jobs
# Bash 5.2 can report a trap parser EOF when HUP interrupts process substitution.
# Snapshot the inherited job table without process substitution or word splitting.
active_jobs=$(jobs -pr) || return 1
while IFS= read -r pid; do
[[ "$pid" == "$expected" ]] && return 0
done < <(jobs -pr)
done <<< "$active_jobs"
return 1
}

Expand Down