From 11b210adcdf6e87aaf2672b2d875e94c4690a000 Mon Sep 17 00:00:00 2001 From: LucaLin <78164141+LucaLin233@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:35:47 +0000 Subject: [PATCH 1/5] test(push): distinguish transfer failures from lifecycle barriers --- tests/test-push.sh | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/test-push.sh b/tests/test-push.sh index b433c16..6adbdbe 100644 --- a/tests/test-push.sh +++ b/tests/test-push.sh @@ -1386,12 +1386,29 @@ EOF runtime_has_published_worker_state && fail "sliding batch left worker-session state" runtime_has_worker_registration_state && fail "sliding batch left registration state" : > "$SUCCESS_FILE"; : > "$FAILED_FILE"; : > "$root/state/current"; : > "$root/state/max" + diagnose_batch_contract() { + printf "DIAG: bash=%s status=%s lifecycle_failed=%s worker_error=%s\n" "$BASH_VERSION" "$1" "$BATCH_WORKER_FAILED" "$BATCH_WORKER_ERROR" >&2 + declare -p ACTIVE_WORKERS ACTIVE_WORKER_STATE_FILES ACTIVE_WORKER_STATE_STARTS REGISTERING_WORKERS WORKER_REGISTRATION_READY_FILES WORKER_REGISTRATION_RELEASE_FILES WORKER_REGISTRATION_STARTS WORKER_REGISTRATION_STAGE_FILES >&2 + } + assert_transfer_failure_contract() { + local label=$1 status=$2 + if [[ $status != 1 || $BATCH_WORKER_FAILED != false || $BATCH_WORKER_ERROR != false ]]; then + diagnose_batch_contract "$status" + fail "$label must be ordinary transfer failure, not lifecycle/accounting failure" + fi + pass "$label preserves ordinary failure status" + } SERVERS=(good1 bad1 good2) - assert_fail "partial server failure returns nonzero" run_transfer source destination + partial_status=0 + run_transfer source destination || partial_status=$? + assert_transfer_failure_contract "partial failure" "$partial_status" + assert_eq 2 "$(wc -l < "$SUCCESS_FILE")" "partial failure completes both unrelated servers" assert_eq 1 "$(wc -l < "$FAILED_FILE")" "partial failure records one failed server" : > "$SUCCESS_FILE"; : > "$FAILED_FILE"; : > "$root/state/current"; : > "$root/state/max" SERVERS=(bad1 bad2) - assert_fail "all server failures return nonzero" run_transfer source destination + all_status=0 + run_transfer source destination || all_status=$? + assert_transfer_failure_contract "all failures" "$all_status" assert_eq 2 "$(wc -l < "$FAILED_FILE")" "all failures record every server" cleanup_runtime ) From b7548d5aa9bb76e1e9ca8bec4cbd3065d1068c51 Mon Sep 17 00:00:00 2001 From: LucaLin <78164141+LucaLin233@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:43:33 +0000 Subject: [PATCH 2/5] test(push): capture first batch lifecycle failure boundary --- tests/test-push.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test-push.sh b/tests/test-push.sh index 6adbdbe..9cc5d3a 100644 --- a/tests/test-push.sh +++ b/tests/test-push.sh @@ -1390,6 +1390,32 @@ EOF printf "DIAG: bash=%s status=%s lifecycle_failed=%s worker_error=%s\n" "$BASH_VERSION" "$1" "$BATCH_WORKER_FAILED" "$BATCH_WORKER_ERROR" >&2 declare -p ACTIVE_WORKERS ACTIVE_WORKER_STATE_FILES ACTIVE_WORKER_STATE_STARTS REGISTERING_WORKERS WORKER_REGISTRATION_READY_FILES WORKER_REGISTRATION_RELEASE_FILES WORKER_REGISTRATION_STARTS WORKER_REGISTRATION_STAGE_FILES >&2 } + # Test-local wrappers preserve return values and only log failing boundaries. + # No argument values, environment dumps, credentials, or global xtrace. + for diagnostic_function in launch_worker prune_active_workers wait_for_worker_slot wait_for_all_workers cleanup_active_failed_worker_sessions; do + eval "$(declare -f "$diagnostic_function" | sed "1s/$diagnostic_function/diagnostic_original_$diagnostic_function/")" + done + diagnostic_boundary() { + local name=$1 status=0 + shift + "diagnostic_original_$name" "$@" || status=$? + if (( status != 0 )); then + printf "DIAG: boundary=%s pid=%s status=%s\n" "$name" "$BASHPID" "$status" >&2 + diagnose_batch_contract "$status" + fi + return "$status" + } + launch_worker() { diagnostic_boundary launch_worker "$@"; } + prune_active_workers() { diagnostic_boundary prune_active_workers "$@"; } + wait_for_worker_slot() { diagnostic_boundary wait_for_worker_slot "$@"; } + wait_for_all_workers() { diagnostic_boundary wait_for_all_workers "$@"; } + cleanup_active_failed_worker_sessions() { diagnostic_boundary cleanup_active_failed_worker_sessions "$@"; } + eval "$(declare -f stop_batch_after_lifecycle_failure | sed "1s/stop_batch_after_lifecycle_failure/diagnostic_original_stop_batch_after_lifecycle_failure/")" + stop_batch_after_lifecycle_failure() { + printf "DIAG: barrier observed before lifecycle teardown\n" >&2 + diagnose_batch_contract "$MANAGED_CLEANUP_FAILURE_STATUS" + diagnostic_original_stop_batch_after_lifecycle_failure + } assert_transfer_failure_contract() { local label=$1 status=$2 if [[ $status != 1 || $BATCH_WORKER_FAILED != false || $BATCH_WORKER_ERROR != false ]]; then From 4e540889b69703bbd3579743b38f0ef278ccf4fd Mon Sep 17 00:00:00 2001 From: LucaLin <78164141+LucaLin233@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:06:58 +0000 Subject: [PATCH 3/5] test(push): bound consecutive failure batch probes --- tests/test-push.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test-push.sh b/tests/test-push.sh index 9cc5d3a..153b478 100644 --- a/tests/test-push.sh +++ b/tests/test-push.sh @@ -1424,6 +1424,12 @@ EOF fi pass "$label preserves ordinary failure status" } + # Fixed probes: stop on first failure, never reset lifecycle barriers. + batch_probe_deadline=$((SECONDS + 90)) + for batch_probe in 1 2 3 4 5 6 7 8; do + (( SECONDS < batch_probe_deadline )) || fail "batch probe budget exhausted" + printf "DIAG: consecutive failure batch round=%s/8\n" "$batch_probe" + : > "$SUCCESS_FILE"; : > "$FAILED_FILE"; : > "$root/state/current"; : > "$root/state/max" SERVERS=(good1 bad1 good2) partial_status=0 run_transfer source destination || partial_status=$? @@ -1436,6 +1442,7 @@ EOF run_transfer source destination || all_status=$? assert_transfer_failure_contract "all failures" "$all_status" assert_eq 2 "$(wc -l < "$FAILED_FILE")" "all failures record every server" + done cleanup_runtime ) From fc9dce8e50972f82664f69efaa2efdb9396ab35c Mon Sep 17 00:00:00 2001 From: LucaLin <78164141+LucaLin233@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:12:19 +0000 Subject: [PATCH 4/5] docs(test): record batch investigation and validate full-suite context --- docs/push-batch-investigation.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/push-batch-investigation.md diff --git a/docs/push-batch-investigation.md b/docs/push-batch-investigation.md new file mode 100644 index 0000000..aea44fa --- /dev/null +++ b/docs/push-batch-investigation.md @@ -0,0 +1,12 @@ +# Push batch lifecycle investigation + +Original failure: main commit 6e24bcbead3d352f305fb92bee9d894de441f554, run 34237080747, Debian full-suite context. +The partial-transfer case entered lifecycle failure before the following batch failed accounting. + +Current probes require exact ordinary failure status and preserve lifecycle barriers. Eight fixed rounds stop on the first failure; no retry-to-green. +Selective push-only runs have not reproduced the original failure. This document is an unknown dependency under the existing tests/run.sh mapping, intentionally exercising the existing full-suite fallback for this diagnostic PR. No workflow or selection rules are changed. + +Capture only Bash version, return codes and lifecycle maps. No credentials, full environment dump or global xtrace. +A green full run is not proof of a fix. Retain original logs and distinguish fixture instrumentation from production behavior. + +Next evidence needed: first failing boundary and pre-teardown state under the full suite. Production code remains unchanged; PR 47 stays blocked pending an explained resolution. From e40536a7d5790788d3b785e8033b4b5f78658267 Mon Sep 17 00:00:00 2001 From: LucaLin <78164141+LucaLin233@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:29:51 +0000 Subject: [PATCH 5/5] test(push): remove intrusive function wrappers from batch probes --- tests/test-push.sh | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/tests/test-push.sh b/tests/test-push.sh index 153b478..fbc675b 100644 --- a/tests/test-push.sh +++ b/tests/test-push.sh @@ -1390,32 +1390,7 @@ EOF printf "DIAG: bash=%s status=%s lifecycle_failed=%s worker_error=%s\n" "$BASH_VERSION" "$1" "$BATCH_WORKER_FAILED" "$BATCH_WORKER_ERROR" >&2 declare -p ACTIVE_WORKERS ACTIVE_WORKER_STATE_FILES ACTIVE_WORKER_STATE_STARTS REGISTERING_WORKERS WORKER_REGISTRATION_READY_FILES WORKER_REGISTRATION_RELEASE_FILES WORKER_REGISTRATION_STARTS WORKER_REGISTRATION_STAGE_FILES >&2 } - # Test-local wrappers preserve return values and only log failing boundaries. - # No argument values, environment dumps, credentials, or global xtrace. - for diagnostic_function in launch_worker prune_active_workers wait_for_worker_slot wait_for_all_workers cleanup_active_failed_worker_sessions; do - eval "$(declare -f "$diagnostic_function" | sed "1s/$diagnostic_function/diagnostic_original_$diagnostic_function/")" - done - diagnostic_boundary() { - local name=$1 status=0 - shift - "diagnostic_original_$name" "$@" || status=$? - if (( status != 0 )); then - printf "DIAG: boundary=%s pid=%s status=%s\n" "$name" "$BASHPID" "$status" >&2 - diagnose_batch_contract "$status" - fi - return "$status" - } - launch_worker() { diagnostic_boundary launch_worker "$@"; } - prune_active_workers() { diagnostic_boundary prune_active_workers "$@"; } - wait_for_worker_slot() { diagnostic_boundary wait_for_worker_slot "$@"; } - wait_for_all_workers() { diagnostic_boundary wait_for_all_workers "$@"; } - cleanup_active_failed_worker_sessions() { diagnostic_boundary cleanup_active_failed_worker_sessions "$@"; } - eval "$(declare -f stop_batch_after_lifecycle_failure | sed "1s/stop_batch_after_lifecycle_failure/diagnostic_original_stop_batch_after_lifecycle_failure/")" - stop_batch_after_lifecycle_failure() { - printf "DIAG: barrier observed before lifecycle teardown\n" >&2 - diagnose_batch_contract "$MANAGED_CLEANUP_FAILURE_STATUS" - diagnostic_original_stop_batch_after_lifecycle_failure - } + # Keep production call stacks intact; diagnose only after return. assert_transfer_failure_contract() { local label=$1 status=$2 if [[ $status != 1 || $BATCH_WORKER_FAILED != false || $BATCH_WORKER_ERROR != false ]]; then