TurnListener implementations are the only thing maintaining the Injector's own invariant #556
Closed
opened 2026-09-12 10:18:53 +02:00 by ltms
·
6 comments
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#556
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?
Follow-up to #553. That ticket stops the live hang; this is the contract defect underneath it, which no ordering fix removes.
Stated by the fleet01 lead, and it is the clearest form of the problem:
The invariant, and who actually keeps it
The
Injectorowns this rule: every delivered turn has a registered waiter. A message that has been typed into a pane must have a rendezvous entry, or the caller that is blocked on it can never be resolved.Measured on
mainat93a9ed3, the only thing that can satisfy that rule lives in another class, behind a callback:So the
Injectorstates an invariant it cannot enforce. Whether it holds depends on a listener implementation returning normally — andFleetd's wiring adds two implementations behind one call, either of which can throw.Why #553's fix does not close this
#553 makes the
finallydo the delivered block's whole job, including callingonDelivered. That fixes the reachable instance. It does not change the shape:Injectorstill depends on a callback to keep its own rule,TurnListeneradded later can break it by throwing, andInjectorhas no way to detect that it was broken.Reordering the post-monitor blocks was considered and rejected (see #553) — and it would not have helped here either. It moves which listener has to behave; the dependency stays.
What is wanted
Make the registration structurally independent of whether any listener throws. The shape to aim for: the
Injectorregisters the turn itself, directly, as part of delivering it, and the listener callback becomes a notification that observers may act on — something that can fail without taking the invariant with it.Whoever takes this should read before choosing a design:
CompletionResolver.captureBaselinedoes two things in one call: it registers the waiter, and it scrapes the pane for the CB-115 staleness baseline. Only the first is theInjector's invariant. The scrape is a herdr round-trip and is allowed to fail — it already has its owncatch (RuntimeException)that fails open withbaseline = null. Splitting those two responsibilities is probably most of the work.onTurnCompletereadsinFlightfor the previous turn (CompletionResolver.java:274-277), so the new turn's entry must not be installed before the previous turn is resolved. Any redesign has to keep that, or it trades this defect for the CB-116 cross-turn stale reply.remove(key, value); onlycaptureBaseline's own no-waiter path uses the one-arg form. That distinction is deliberate — do not flatten it.Acceptance
TurnListenerthat throws from every callback, asserting a delivered turn is still registered and its waiter still resolvable. This must be red before the change.StatusPoller'scatch (Throwable)and log at ERROR. A listener bug stays loud.sedonly, pristine anchor counted 1 → 0 before and after, red with that test's own message, restored byte-identical under a fullshasum -a 256, green control re-run.Out of scope
Related: #553 (the reachable instance and the ordering constraint), #512 (one symbol carrying two states), #538 / #543 (the
catch (Throwable)work this sits on top of).How to test the conditional removes, since "is that expressible without being brittle?" was left open on #553
Answer: yes — by not testing the idiom at all. Credit to the fleet01 lead for the framing; the line-level check below is mine, measured on
mainat26f380a.Do not test the idiom
A textual or AST check for
remove(x, y)is brittle, reads as ceremony, and gets deleted by the next person who finds it. It also breaks on any refactor that changes how the removal is spelled without changing what it does.Test the invariant the idiom exists to maintain
Name it first:
That is a property worth a test on its own merits, and it happens to be exactly what a one-arg
removebreaks. The test shape needs no reflection and never peeks atinFlight:remove(T, A): no-op, B survives, the assertion passes.remove(T): evicts B,resolvefinds no entry and returns silently, B's waiter never fires — red.Behavioural, small, and it survives a refactor.
The general rule, which is the part worth carrying
When a defect has no symptom because an idiom is load-bearing, do not test the idiom — name the invariant the idiom exists to maintain, and test that. And if you cannot name the invariant in a sentence, that is the signal the idiom may be incidental rather than load-bearing, which is a thing to settle before writing any test. Here it names in one sentence, so it is load-bearing and the test earns its place.
One correction to the proposed test count
The suggestion was "three terminal methods —
resolve,noReportMessage,fail— so three tests, one each". Measured, there are only two reachable entry points:noReportMessageis private and called only from insideresolve. So its two removes (:478,:498) are reached by drivingresolvedown that sub-path, not by calling it directly. The coverage map is:resolve:308:378:405:418, plus:478:498vianoReportMessagefail:522:539:582Three tests is still the right number — one for the plain
resolvepath, one for thenoReportMessagesub-path, one forfail— but the third is a sub-path of the first, not a separate method. Between them they cover all nine conditional sites.Scope
This belongs to whoever takes this ticket, as a non-goal-to-break: the redesign here must not flatten a two-arg remove into a one-arg one. Adding the three tests first, before the redesign, is the cheaper order — they are red-able against a deliberate one-arg mutation today, which proves they work before anything moves.
Unblocked, plus one addition to Acceptance
Unblocked. #553 merged as
f606fccand is onmain. The "merge that first" line in Out of scope is satisfied.Addition to Acceptance — this ticket must delete #561's test
#561 is now open:
Fleetd.java:514-515callscompletion.onDeliveredthensessions.onDelivered, and that order is load-bearing and untested. Swap the two lines andsessionsthrows before the resolver ever registers — the caller then burns its full timeout, with the whole suite green.#561 will add an order-dependent test: a listener positioned after the resolver throws, assert the record exists, prove it red on the two-line swap.
The fleet01 lead flagged the trap in that, and it belongs in this ticket's acceptance rather than only in #561's:
So, added to Acceptance:
TurnListenerthat throws from every callback, with the delivered turn still registered and its waiter still resolvable — is that form, and it is red today precisely because registration currently lives inside a callback. Make the replacement explicit in the PR so the two cannot both survive: a green order test sitting next to the real one is an argument that this ticket was unnecessary.Fleetd.javathe order test was pinning, and why it is no longer needed. After this change the order stops mattering, and a reader a month from now must be able to see that the guard was removed because the thing it guarded was fixed — not dropped.Nothing else changes. The CB-116 ordering constraint from #553 still stands, the conditional two-arg
removedistinction still stands, and the original throwable must still reachStatusPoller'scatch (Throwable)and log at ERROR.The three-tests-nine-sites design in comment 16908 is unaffected.
Sharpening acceptance criterion 5 — "red today" is not executable after the merge
Raised by the fleet01 lead, correcting wording of mine. This is a correction to the brief; it binds, and it is on the ticket because the worker is mid-turn and a push would not reach them.
I wrote that the order-independent replacement test must be red today, and that a replacement which is not red today is not a replacement. That is right about the property and wrong about when you can check it:
Exactly so. Once
Injectorowns the registration, the listener-throws test passes, and nobody afterwards can tell whether it ever would have failed. A test that has only ever been observed green is indistinguishable from a test that cannot fail.The criterion, in its executable form
The replacement test must be shown RED on a tree WITHOUT the fix, and the PR must carry that output.
Concretely, before you commit the fix:
TurnListenerthat throws from every callback, asserting the delivered turn is still registered and its waiter still resolvable.This is the same discipline as a mutation kill, and for the same reason: the state that produced the evidence will not exist again once the change is merged. A mutation kill puts the red output in the PR because the mutated tree is thrown away. A red-before-fix test is the identical case — the unfixed tree is thrown away by the merge.
If the test passes at step 2, stop and say so in the PR rather than proceeding. A green test at that point means one of two things, and both matter: either the contract defect is not reachable the way the ticket describes, or the test does not exercise it. Either one is a finding worth more than a merge.
Acceptance criteria 1–6 are otherwise unchanged.
Criterion 5, step 2: "the test passes" is a SUCCESS outcome, not an exception clause
Raised by the fleet01 lead, correcting the shape of my own wording in comment 16974. This is a correction to the brief; it binds.
I put the stop condition in the last paragraph of 16974, after the four numbered steps:
That is the right instruction in the wrong position. fleet01's point:
So, stated plainly:
If the order-independent test is GREEN at step 2 — on the tree with no production change applied — that is a successful outcome of this ticket. Report it and stop. Do not adjust the test until it goes red, and do not proceed to the fix.
A green test at step 2 means one of two things, and both are worth more than the merge:
Either way the ticket's premise is wrong, and finding that out is the valuable result. Nobody is graded down for it. What would be a bad outcome is quietly reshaping the test until it goes red, because then the red proves the reshaping, not the defect.
What to report if that happens
Post it on this ticket and in your
fleet_reply, with:I have a peer lead waiting specifically on this result — the fleet01 lead wrote the contract sentence this ticket rests on, and told me:
So this outcome has a reader and a consequence. It is not a way of failing quietly.
Unchanged
The red path is still the expected one, and criteria 1–6 are otherwise exactly as written in comments 16908, 16966 and 16974. If the test goes red at step 2, carry on and paste the failure output as criterion 5 already requires: test name, its own assertion message, and the observed value.
One reminder that applies either way: take the test count from
fleetd/target/surefire-reports/*.txt, not from a pipe, and print the build's exit code next to it.mvn -qsuppresses the count entirely, which looks identical to a pass with no evidence.Rework on PR #566 — the ticket's own invariant is unpinned on the recovery path
The design is right and the ordinary path is properly pinned. One gap, and it is this ticket's own invariant surviving on the path #553 exists for.
What I verified first (all clean)
Merged
worker/556-injector-owns-registration-e027a5-1intoorigin/main(f4f5f31) →52f018e.mvn -o install→ exit 0, 1749 tests, 0 failures, 0 errors (main is 1744, so +5). Maven's summary and an independent sum over the 130 surefire reports agree.Injector.javapristine sha8fcb698afccc254b0c99d3a4bf9e960c0e7c85e542024e870c1e815dce6d62a9— matches the sha you reported, independently confirmed.new Injector(...)exists (Fleetd.java:534) and it uses the explicit 5-arg registrar overload, so theinstanceof TurnRegistrar ? r : NOOPfallback is test-only. Good.Injector.java:660, exact-line anchor 1 → 0) turns both new tests red:Note for the PR: the literal form of criterion 5 — run the test on a tree without the fix — is not executable here, because both tests call the new 5-arg constructor and cannot compile against
origin/main. The above is the faithful equivalent: it produces the same end state (no registration) that the pre-fix code produced when the listener threw. Say that explicitly in the PR rather than implying the literal check was run.The gap:
Injector.java:703There are two
registrar.register(target, sent.token())call sites. You mutated:660. I mutated:703, the #553finallybackstop:It survived the full suite.
This is not an unreachable path. I instrumented the line and re-ran the suite:
So the suite executes that registration twice and asserts nothing about it. Coverage without an assertion, which is the one combination that reads as protected and is not.
Why it matters here specifically: the backstop runs only when an earlier block threw before the ordinary path ran, so
:703is the only thing that registers the turn in that scenario. Delete it and a turn delivered on the recovery path is never registered — its waiter is never resolvable and the caller burns its full timeout. That is precisely the defect this ticket exists to remove, still live on the path #553 was written for. Your own code comment at:700-702states the invariant ("nothing has registered this turn yet"), which makes it a free test case.The existing #553 tests do drive this path — they assert the delivered future completes, never that the turn is registered. Same lines executed, different property.
Acceptance for the rework
onStatusso an earlier block throws before the ordinary delivery path runs (the existing #553 tests aroundInjectorTest:810-841already construct that situation), then assertcompletion.inFlight(target)is non-null and carries the exact waiter — the same assertion the ordinary-path test makes, on the recovery path.Injector.java:703and nothing else. Report the exact-line anchor count moving 1 → 0, the test name, its own assertion message, and the observed value. Restore and confirm the sha returns to8fcb698a....fleetd/target/surefire-reports/*.txt.Nothing else changes. The design, the
TurnRegistrarseam, the CB-116 ordering test and the threeCompletionResolvertests all stand.Credit where it is due
Two things in your report were exactly right and are worth repeating. Your third mutation did not kill its test on the first attempt; you diagnosed why (the conditional remove is already a no-op against a superseded map value) and corrected the mutation to the real defect — the one-arg flattening, anchor 3 → 2 — instead of reporting a survivor or quietly reshaping the test. And you flagged that mutation 2 could not be expressed as a pure reorder, so you used the call-removal form and said so. Both are the behaviour I want. This finding is the same discipline applied to the half you did not mutate, which is my job, not a criticism of yours.
Merged as
db4c98a(PR #566). Closing.What the rework had to fix
The first pass moved registration onto its own
TurnRegistrarseam and pinned it with two tests. Both tests aimed at the ordinary delivery path.Injector.javahas tworegistrar.registercall sites, and the second one — the fleetd #553finallybackstop — had nothing asserting registration on it.That line was not uncovered. I instrumented it and re-ran the suite: it executes twice, and nothing asserts anything about it. Covered but unasserted is the one combination that reads as protected and is not, because a coverage number says yes and a green suite says yes.
Verification (lead, on a tree with main merged in)
Build:
mvn -o clean installexit 0. 1750 tests, agreed by Maven's own summary line and an independent sum over 130target/surefire-reports/*.txtfiles. Re-ran on realmainafter the merge: same 1750, exit 0.Each call site is now independently pinned. Both mutations used exact full-line anchors, count 1 -> 0, and
Injector.javarestored to8fcb698afccc254b0c99d3a4bf9e960c0e7c85e542024e870c1e815dce6d62a9after each:InjectorTest:703(the #553 backstop)aRuntimeExceptionFromOnTurnCompleteStillLeavesTheNextDeliveryRegisteredOnTheRecoveryPath:660(the ordinary path)The second row is the part that matters. It shows the new test is not simply re-covering the ordinary path: it goes red for
:703alone and green for:660alone. The test also assertsinFlight.waiter()is"second"'s own waiter, so a stray registration from some other turn cannot satisfy it.Failure message on the
:703mutation:One limit, stated rather than implied
The literal form of criterion 5 — compile the new tests against the tree at the branch point — cannot be run. Both tests call the 5-argument
Injectorconstructor, which does not exist before this change, so they do not compile there. What was run instead is the faithful equivalent: remove the registration call on the fixed tree, which reproduces the same end state (no registration) the pre-fix code produced when a listener threw. Same evidence, one inference step longer. The PR says so too.Follow-ups this unblocks
Injector.javasettled. It is free now.InjectorTestcases are the order-independent replacement. #561 should be closed or rewritten around that.Fleetd.java. That collision is gone.