The swap guard can be disabled with the suite green — a "successful" redeploy that never puts the new jar in place #521
Closed
opened 2026-09-12 06:29:41 +02:00 by ltms
·
1 comment
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#521
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Flagged by the #520 worker as an out-of-scope observation, and I reproduced it myself before
filing. This is the same shape as #517 defect 1, on a branch whose failure is worse.
The mutation
scripts/redeploy-fleetd.sh:723, immediately above the swap:Proven applied, four ways:
Result:
Restored byte-identical to
2cb83dc380c7226191d657c40fccdfc856904e40b2d03d0851fb6e522cee2f41,control rerun exit 0.
Harness proof, on my own invocation of the suite, because a green result is unreadable without
one. I re-applied the
jar_idabsent→presentmutation that #520 had just pinned:So the suite can fail when I run it. It does not fail for the swap guard.
What the mutant does on a live redeploy
The build runs.
stage_built_jarmoves the freshly built jar off the live path to$JAR_STAGED(
:621). The daemon stops.wait_for_daemon_exitconfirms the old pid is gone. The swap neverhappens. Then the start step runs.
So the operator sees a redeploy that reports every step succeeding, and the daemon either fails to
start at all —
$JARno longer exists, because staging moved it — or, worse, starts on whateverstale jar happened to still be at the live path.
That is #493's outcome reached by a different route. #493 was "the build writes into the path a
running process holds"; this is "the staged jar is never put back". Both end with the daemon not
running the code that was just built, and both report success.
Why nothing catches it
test_swap_ordered_after_wait_and_before_startis a source-shape test. It checks the relativeline positions of the call sites in the script's own text — that
swap_staged_jarappears afterwait_for_daemon_exitand before the start step.Mutating
if [ "$DO_BUILD" = 1 ]toif falseis a one-line in-place edit. Every line position thetest looks at is unchanged. The test passes, and it was never able to fail for this.
This is #517's finding exactly, one branch over: a source-text or source-position assertion pins
what the file says and can never pin whether the code is reached. #517 named it as the fifth
member of the vacuous-test family. This is its second instance in the same file, and the worker found
it by being asked to look for the shape rather than the instance.
Severity
Higher than #517 defect 1. That one produced a wrong abort message during an abort the operator had
already chosen. This one produces a silent wrong outcome during a redeploy the operator believes
succeeded, and the next thing that happens is the fleet running code nobody intended.
The one mitigation already in the script: the final verify step checks
/healthzand the jar id. Itis worth checking whether that would catch this mutant — if
ok "jar in place: $(jar_id)"is insidethe same disabled block, the reported jar id is never printed either, and the summary line's jar id
comes from elsewhere. Someone should establish that rather than assume it.
The fix
Same as #517 defect 1, and the precedent is now in the file twice (
wait_for_daemon_exit,drain_gate_refusal): extract the decision so the suite can call it.Something like
should_swap(do_build)returning a status, with the main flow calling it, plus testsfor both values. Keep
test_swap_ordered_after_wait_and_before_start— it catches a re-ordering,which the new test cannot.
Then sweep the rest of the file for the same shape and report what you find: any branch whose only
test is a grep of this script's own source. Do not fix those in this ticket; list them.
Acceptance
bash scripts/test-redeploy-fleetd.shexits 0, with the test count up by the number added. Reporttest functions defined and invoked; they must match.
bash -npasses on both scripts.code, restore, confirm byte-identical with
shasum -a 256, then a green control run.grep -nre-read of the line. A
$-variable inside a double-quoted$( )is expanded by your own shellbefore grep sees it, so both greps come back empty and it looks like the mutation never applied.
A genuinely un-applied mutation gives 0 and 1, never 0 and 0. Use single quotes.
scripts/redeploy-fleetd.shagainst the live daemon, with any flag. It is thechannel the fleet talks through.
Related
Closed by #526 (merged). Ticked against this ticket's own fix and acceptance sections.
The fix section
should_swap(do_build)returning a status, with the main flow calling it, plus tests for both values"test_swap_ordered_after_wait_and_before_start, since it catches a re-ordering the new test cannotswap_if_built "$DO_BUILD"But the fix this ticket prescribed does not close this ticket, and that is my error. Extracting a predicate pins the decision and never the wiring: with the main flow reading
if should_swap "$DO_BUILD"; then, changing it toif false; thenleft the whole suite at exit 0 with zero FAIL lines. The implementer measured that, reported it in the PR body, and correctly declined to fix it because the ticket asked for the predicate shape. I reproduced it independently, then finished it at the gate.What actually closes it: the decision and the action now live together in
swap_if_built(), which the main flow calls unconditionally, so there is no guard left in the main flow to get wrong. Two tests drive it with a recording stub in place of the realmv, so they fail if the guard is removed, inverted, or stops being consulted.should_swap()stays as the named decision with its own tests, but is no longer the only thing tested.Acceptance
suite exits 0, count up by the number added, defined = invoked — exit 0, 0
^FAIL:lines, 44 defined / 44 invoked (was 40/40 on main at c71ac23; +2 from the implementer, +2 from me).bash -non both scripts — rc=0 under/bin/bash3.2.57 andbash5.3.9.the mutation is killed — the literal mutation in this ticket no longer exists, because the guard it edits is gone from the main flow. Four mutations covering the same behaviour are killed instead, each with its own named FAIL line, each restored byte-identical by hash, green control after the battery:
swap_if_builtswap_if_built 0 (--no-build; nothing was staged this run) must not swap, but it didswap_if_built 1 (a build ran and staged a jar) must perform the swap, and did notshould_swap's comparison changedshould_swap 1 (a build ran and staged a jar) must return truecould not find the swap call site in redeploy-fleetd.shtwo greps with different strings, plus a
grep -nre-read — done. A correction to my own first battery: three cells read "original gone = 0" because I left\"inside an already-single-quoted pattern, so the backslashes went into the pattern and matched nothing. That is a false zero from a different cause than the$-expansion trap this ticket warns about, with the identical signature. Re-proved with correct patterns plus a control showing each matches 1 in the unmutated file. Worth adding to the warning: both quoting mistakes give 0 and 0.did not run
redeploy-fleetd.shagainst the live daemon — correct, neither of us did, with any flag.One correction to this ticket's own text
The severity section says this produces "a silent wrong outcome during a redeploy the operator believes succeeded". That is wrong, and the implementer established it rather than assuming, which is what the ticket asked for.
stage_built_jaris unconditional, so$JARis already absent by the swap step;java -jarfails at once and the script dies atno process appearedor/healthz never answered within ${HEALTH_WAIT}s, both printing the daemon log tail. It is a loud failure. The defect was real — a guard disableable with a green suite — but it never reported success. Not re-verified by me, and not run against the live daemon.Found while verifying, fixed here
The ordering test's three
[ -n ... ] || fail "could not find … call site"guards were dead code. Underset -euo pipefailan absent needle fails the assignment andset -ekills the suite before the guard runs — measured: deleting the swap call gave exit 1 with zero bytes of output and no FAIL line. Each grep now ends|| true. Same deletion now names the missing call site. The pattern may exist in other scripts; flagged in #528 for a sweep.Follow-up
#528 — the same unpinned-call-site shape on
drain_gate_refusal, which can silently undo #517 one day after #520 merged, plus thepipefaildead-check sweep.