Verified independently in my own worktree at the pushed head 7c34e8f, not taken
from the worker's report. CI run 1781: success.
The shape is the one #528 asked for and the one #526 arrived at: refuse_drain_gate
composes the message via drain_gate_refusal AND calls die itself, and the main
flow calls it unconditionally at :710. No guard is left in the main flow to
remove, invert or bypass on its own.
My measurements:
test functions defined / invoked 49 / 49 (was 44/44; +5)
bash -n, /bin/bash 3.2.57 rc=0 on both files
bash -n, env bash 5.3.9 rc=0 on both files
clean control exit 0, 0 lines matching ^FAIL:, 256 bytes
Two mutations, both killed, each by a differently named failure:
call site deleted (the item-1 mutation: :710 replaced by a flat
die "aborted — nothing changed")
-> exit 1, 1 ^FAIL: line, 87 bytes
FAIL: could not find the main flow's refuse_drain_gate call site in redeploy-fleetd.sh
refuse_drain_gate stops consulting the predicate (its body's
die "$(drain_gate_refusal ...)" replaced by a flat message)
-> exit 1, 1 ^FAIL: line
FAIL: refuse_drain_gate build-ran+staged-present die message does not name the staged jar
The first cell is the point of the ticket. Before this change the same mutation
gave exit 0, zero FAIL lines and output byte-identical to a clean run at 256
bytes. It now exits 1 and names the missing call site. Each mutation was proven
applied with a uniquely tagged marker plus a second, different search string,
with a control against a pristine copy showing the exact inverse (1/0 mutated,
0/1 pristine), and the function definition confirmed still present so the
mutation targeted the call and not the function. Restored byte-identical to
0e5a99a22c9c65f72960d8f179ca5299307889e06bc42131f098a513e7b97bd6 and the final
control is green.
Needle uniqueness checked, because this is where it could have gone wrong:
grep -cF 'refuse_drain_gate "$DO_BUILD" "$JAR_STAGED"' on the production script
returns 1, at :710, the real call site. The worker hit the self-match trap while
writing the comment above refuse_drain_gate — their first draft quoted the
call-site string literally, which would have let the source-text test match the
comment instead of the call — caught it themselves, and reworded so the comment
cannot become a second match. That is the same trap that cost me a false pass on
a probe earlier today, and catching it unprompted is the better half of this PR.
The dead-check sweep came back as a real negative, with the reasoning shown
rather than asserted: of the five scripts under set -e with pipefail, every
pipe-into-assignment already carries || true or || echo, and the remaining two
scripts have no pipe-into-assignment at all. probe-member-credentials.sh and
deploy/herdr-inner.sh correctly excluded for not having set -e. No live
instances.
One inaccuracy in the report, in the report only: it abbreviates the restored
hash as "0e5a99a2...78f0a", and that tail does not occur in the actual hash,
which ends b97bd6. I hashed the committed file myself and confirmed the restore
matched, so the file is right and only the quoted abbreviation is wrong. Flagged
because an abbreviated hash that nobody can match against anything is worse than
no hash.
wait_for_daemon_exit's call site (item 2) stays open as the ticket scoped it —
source-text pinned only, "partially pinned, not audited". The seven untested
main-flow decisions are untouched; the worker correctly notes it changed the body
of one of those if blocks while leaving the guard condition itself untested, as
instructed.
This commit was merged in pull request #532.
This commit is contained in:
@@ -519,6 +519,32 @@ drain_gate_refusal() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# fleetd #528 — drain_gate_refusal above is well tested (four cases, all direct), but nothing made
|
||||||
|
# the MAIN FLOW's abort actually consult it. Before this, the main flow read
|
||||||
|
# `die "$(drain_gate_refusal "$DO_BUILD" "$JAR_STAGED")"` directly, and mutating that one line to a
|
||||||
|
# flat `die "aborted — nothing changed"` left the whole suite at exit 0 with zero FAIL lines and
|
||||||
|
# byte-identical output to a clean run — every one of drain_gate_refusal's own tests still passed,
|
||||||
|
# because they call the predicate directly and never touch this call site. That silently reinstated
|
||||||
|
# the exact defect #517 was filed to fix. Same shape as #521/#526's should_swap/swap_if_built: a
|
||||||
|
# predicate alone is not enough, because a test proving the predicate is right cannot also prove the
|
||||||
|
# main flow consults it. So the decision (drain_gate_refusal) and the action (die) now live together
|
||||||
|
# in ONE function, and the main flow calls it unconditionally instead of building the die() call
|
||||||
|
# itself — there is no guard left in the main flow to remove, invert, or bypass independently of this
|
||||||
|
# function. drain_gate_refusal stays separate and separately tested because the message-selection
|
||||||
|
# logic is worth naming and testing on its own; refuse_drain_gate is the only thing that ever dies.
|
||||||
|
#
|
||||||
|
# What the behavioural tests above still cannot pin on their own: deleting the call to this function
|
||||||
|
# from the main flow altogether — they call refuse_drain_gate directly, never through the main flow,
|
||||||
|
# because sourcing stops before the main flow ever runs (see the SOURCED guard below). That gap is
|
||||||
|
# closed the same way swap_if_built's is: test_refuse_drain_gate_call_site_present greps this script
|
||||||
|
# for the real invocation, the same shape test_swap_ordered_after_wait_and_before_start already uses
|
||||||
|
# for the swap call. Deliberately NOT written out here as a literal quoted string, so this comment
|
||||||
|
# itself can never become a second match for that test's needle.
|
||||||
|
refuse_drain_gate() {
|
||||||
|
local do_build="$1" staged_path="$2"
|
||||||
|
die "$(drain_gate_refusal "$do_build" "$staged_path")"
|
||||||
|
}
|
||||||
|
|
||||||
# CB-600: sourceable for testing. When this file is SOURCED (not executed) it stops here — nothing
|
# CB-600: sourceable for testing. When this file is SOURCED (not executed) it stops here — nothing
|
||||||
# below runs — so a test harness can `source` it to call check_log_path_matches_plist (or the
|
# below runs — so a test harness can `source` it to call check_log_path_matches_plist (or the
|
||||||
# other pure helpers above) against a throwaway plist fixture without ever reaching the mutating
|
# other pure helpers above) against a throwaway plist fixture without ever reaching the mutating
|
||||||
@@ -677,10 +703,11 @@ if [ -n "$OLD_PID" ] && [ "$ASSUME_YES" = 0 ]; then
|
|||||||
echo
|
echo
|
||||||
read -r -p " Fleet drained? type yes to restart: " reply
|
read -r -p " Fleet drained? type yes to restart: " reply
|
||||||
if [ "$reply" != "yes" ]; then
|
if [ "$reply" != "yes" ]; then
|
||||||
# fleetd #493 / #517: "nothing changed" would be a lie once a build has run and staged a jar —
|
# fleetd #493 / #517 / #528: "nothing changed" would be a lie once a build has run and staged a
|
||||||
# see drain_gate_refusal above for the full decision and why each of its four cases reads the
|
# jar — see drain_gate_refusal above for the full decision and why each of its four cases reads
|
||||||
# way it does.
|
# the way it does. refuse_drain_gate composes that message AND calls die itself, so this guard
|
||||||
die "$(drain_gate_refusal "$DO_BUILD" "$JAR_STAGED")"
|
# has nothing left of its own to get wrong beyond whether it calls refuse_drain_gate at all.
|
||||||
|
refuse_drain_gate "$DO_BUILD" "$JAR_STAGED"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -521,6 +521,106 @@ test_drain_gate_refusal_no_build_staged_absent() {
|
|||||||
assert_equals "aborted — nothing changed" "$result" "no-build+staged-absent refusal wording"
|
assert_equals "aborted — nothing changed" "$result" "no-build+staged-absent refusal wording"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# fleetd #528 — the four tests above pin drain_gate_refusal(), and that is ALL they pin: they call
|
||||||
|
# the predicate directly and never touch the main flow's call site. That was measured to be not
|
||||||
|
# enough, the same way test_should_swap_true_when_build_ran/test_should_swap_false_when_build_skipped
|
||||||
|
# were not enough for #521: with the main flow reading `die "$(drain_gate_refusal "$DO_BUILD"
|
||||||
|
# "$JAR_STAGED")"`, replacing that whole line with a flat `die "aborted — nothing changed"` left this
|
||||||
|
# suite at exit 0 with zero FAIL lines and byte-identical output to a clean run. Nothing above could
|
||||||
|
# tell the difference, because none of it calls anything at or above the call site itself.
|
||||||
|
#
|
||||||
|
# So these four call refuse_drain_gate() — the function the main flow actually calls, holding the
|
||||||
|
# composed message and the die() together — with die() stubbed to RECORD whether it was called and
|
||||||
|
# with what message, instead of exiting the process. That fails if refuse_drain_gate stops consulting
|
||||||
|
# drain_gate_refusal, mangles what it passes it, or simply never calls die.
|
||||||
|
#
|
||||||
|
# What none of these four can catch: deleting the `refuse_drain_gate "$DO_BUILD" "$JAR_STAGED"` line
|
||||||
|
# from the main flow altogether — see the comment above refuse_drain_gate in redeploy-fleetd.sh for
|
||||||
|
# why no test in this file can do better than that (sourcing stops before the main flow runs).
|
||||||
|
DIED_CALLED=0
|
||||||
|
DIED_MESSAGE=""
|
||||||
|
stub_die_recorder() {
|
||||||
|
DIED_CALLED=0
|
||||||
|
DIED_MESSAGE=""
|
||||||
|
die() { DIED_CALLED=1; DIED_MESSAGE="$*"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
test_refuse_drain_gate_build_ran_staged_present() {
|
||||||
|
source "$ROOT/scripts/redeploy-fleetd.sh"
|
||||||
|
local dir staged
|
||||||
|
dir="$TMP/refuse-drain-build-staged"; mkdir -p "$dir"
|
||||||
|
staged="$dir/fleetd-new.jar"
|
||||||
|
printf 'staged jar bytes' > "$staged"
|
||||||
|
stub_die_recorder
|
||||||
|
refuse_drain_gate 1 "$staged"
|
||||||
|
[ "$DIED_CALLED" = 1 ] \
|
||||||
|
|| fail "refuse_drain_gate build-ran+staged-present must call die, and did not"
|
||||||
|
printf '%s' "$DIED_MESSAGE" | grep -qF "$staged" \
|
||||||
|
|| fail "refuse_drain_gate build-ran+staged-present die message does not name the staged jar"
|
||||||
|
printf '%s' "$DIED_MESSAGE" | grep -qF 'Rerun WITHOUT --no-build' \
|
||||||
|
|| fail "refuse_drain_gate build-ran+staged-present die message is missing the rerun instruction"
|
||||||
|
if printf '%s' "$DIED_MESSAGE" | grep -qF 'nothing changed'; then
|
||||||
|
fail "refuse_drain_gate build-ran+staged-present must not claim nothing changed — the jar already moved"
|
||||||
|
fi
|
||||||
|
source "$ROOT/scripts/redeploy-fleetd.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_refuse_drain_gate_build_ran_staged_absent() {
|
||||||
|
source "$ROOT/scripts/redeploy-fleetd.sh"
|
||||||
|
local dir
|
||||||
|
dir="$TMP/refuse-drain-build-no-staged"; mkdir -p "$dir"
|
||||||
|
stub_die_recorder
|
||||||
|
refuse_drain_gate 1 "$dir/fleetd-new.jar"
|
||||||
|
[ "$DIED_CALLED" = 1 ] \
|
||||||
|
|| fail "refuse_drain_gate build-ran+staged-absent must call die, and did not"
|
||||||
|
assert_equals "aborted — nothing changed" "$DIED_MESSAGE" "refuse_drain_gate build-ran+staged-absent die message"
|
||||||
|
source "$ROOT/scripts/redeploy-fleetd.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_refuse_drain_gate_no_build_staged_present() {
|
||||||
|
source "$ROOT/scripts/redeploy-fleetd.sh"
|
||||||
|
local dir staged
|
||||||
|
dir="$TMP/refuse-drain-no-build-staged"; mkdir -p "$dir"
|
||||||
|
staged="$dir/fleetd-new.jar"
|
||||||
|
printf 'leftover staged jar bytes' > "$staged"
|
||||||
|
stub_die_recorder
|
||||||
|
refuse_drain_gate 0 "$staged"
|
||||||
|
[ "$DIED_CALLED" = 1 ] \
|
||||||
|
|| fail "refuse_drain_gate no-build+staged-present must call die, and did not"
|
||||||
|
assert_equals "aborted — nothing changed" "$DIED_MESSAGE" "refuse_drain_gate no-build+staged-present die message"
|
||||||
|
source "$ROOT/scripts/redeploy-fleetd.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_refuse_drain_gate_no_build_staged_absent() {
|
||||||
|
source "$ROOT/scripts/redeploy-fleetd.sh"
|
||||||
|
local dir
|
||||||
|
dir="$TMP/refuse-drain-no-build-no-staged"; mkdir -p "$dir"
|
||||||
|
stub_die_recorder
|
||||||
|
refuse_drain_gate 0 "$dir/fleetd-new.jar"
|
||||||
|
[ "$DIED_CALLED" = 1 ] \
|
||||||
|
|| fail "refuse_drain_gate no-build+staged-absent must call die, and did not"
|
||||||
|
assert_equals "aborted — nothing changed" "$DIED_MESSAGE" "refuse_drain_gate no-build+staged-absent die message"
|
||||||
|
source "$ROOT/scripts/redeploy-fleetd.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fleetd #528 — closes the one gap the four behavioural tests above cannot: they call
|
||||||
|
# refuse_drain_gate directly, and sourcing stops before the main flow ever runs (the SOURCED guard),
|
||||||
|
# so none of them can prove the main flow still CALLS refuse_drain_gate at all. Same shape as
|
||||||
|
# test_swap_ordered_after_wait_and_before_start: a source-text grep for the real call site. This is
|
||||||
|
# what actually kills the item-1 mutation from the ticket — replacing the main flow's call with a
|
||||||
|
# flat `die "aborted — nothing changed"` removes this exact needle, where none of the behavioural
|
||||||
|
# tests above would even notice.
|
||||||
|
#
|
||||||
|
# The grep ends `|| true`: this file runs under `set -euo pipefail`, so an ABSENT needle would fail
|
||||||
|
# the assignment and `set -e` would kill the whole suite before the `[ -n ... ] || fail` guard below
|
||||||
|
# ever ran — the exact dead-check shape fleetd #528 also flags as a sweep finding (see the PR body).
|
||||||
|
test_refuse_drain_gate_call_site_present() {
|
||||||
|
local src="$ROOT/scripts/redeploy-fleetd.sh" call_line
|
||||||
|
call_line="$(grep -Fn 'refuse_drain_gate "$DO_BUILD" "$JAR_STAGED"' "$src" | head -1 | cut -d: -f1 || true)"
|
||||||
|
[ -n "$call_line" ] \
|
||||||
|
|| fail "could not find the main flow's refuse_drain_gate call site in redeploy-fleetd.sh"
|
||||||
|
}
|
||||||
|
|
||||||
test_no_errors() {
|
test_no_errors() {
|
||||||
cat > "$TMP/no-errors.log" <<'LOG'
|
cat > "$TMP/no-errors.log" <<'LOG'
|
||||||
2026-09-05 12:00:00 INFO fleetd listening
|
2026-09-05 12:00:00 INFO fleetd listening
|
||||||
@@ -753,6 +853,11 @@ test_drain_gate_refusal_build_ran_staged_present
|
|||||||
test_drain_gate_refusal_build_ran_staged_absent
|
test_drain_gate_refusal_build_ran_staged_absent
|
||||||
test_drain_gate_refusal_no_build_staged_present
|
test_drain_gate_refusal_no_build_staged_present
|
||||||
test_drain_gate_refusal_no_build_staged_absent
|
test_drain_gate_refusal_no_build_staged_absent
|
||||||
|
test_refuse_drain_gate_build_ran_staged_present
|
||||||
|
test_refuse_drain_gate_build_ran_staged_absent
|
||||||
|
test_refuse_drain_gate_no_build_staged_present
|
||||||
|
test_refuse_drain_gate_no_build_staged_absent
|
||||||
|
test_refuse_drain_gate_call_site_present
|
||||||
test_no_errors
|
test_no_errors
|
||||||
test_recovery_patterns_match_source
|
test_recovery_patterns_match_source
|
||||||
test_attributed_recovered_connection_error
|
test_attributed_recovered_connection_error
|
||||||
|
|||||||
Reference in New Issue
Block a user