fleetd #494: log measured elapsed time, not the configured budget, on lead-rollover failure/success #496

Merged
ltms merged 3 commits from worker/494-1015ce-2 into main 2026-09-12 04:44:10 +02:00
Member

fleetd #494 — log the MEASURED elapsed time, never the configured budget

Goal: when a lead-session roll goes wrong, the daemon log must print the time it
actually waited, never the time it was configured to wait.

What changed (one file of production code + its test)

fleetd/src/main/java/dev/ltms/fleet/lead/LeadRollover.java:

  1. runRollover's /clear-timeout log.warn (was ~line 400) now prints
    configured=<Ns>s elapsed=<M>ms nudges=<K>, each labelled, instead of presenting
    cfg.clearSettleSeconds() as the wait duration.
  2. waitForClearPickupAndSettle's pickup-grace release (was ~line 569) is now
    log.warn (was log.info) and prints the measured elapsed time next to the
    target pane. The release itself (returning "settled" rather than wedging the
    roll) is unchanged — only the log level and content.
  3. The success line lead-rollover: rolled (was ~line 406) now prints the measured
    elapsed time for the whole roll.
  4. waitForClearPickupAndSettle now returns a private ClearSettleResult(settled, elapsedMillis, nudges) record instead of a bare boolean, computed from the
    already-injected nowMillis clock — no new field, no new constructor parameter.

No behaviour change: same sends, same order, same release/refuse decisions —
only what gets logged changed. All 29 original LeadRolloverTest tests still pass
unmodified.

Tests added (3, one per changed line, in LeadRolloverTest.java)

  • clearTimeoutLogPrintsMeasuredElapsedAndNudgesNotJustConfigured — pins item 1:
    asserts the WARN line contains configured=1s, elapsed=1500ms, nudges=0 and
    does NOT contain within 1s.
  • clearGraceReleaseLogIsWarnWithMeasuredElapsed — pins item 2: asserts the release
    line is Level.WARN (not INFO) and contains elapsed=4500ms plus the target pane.
  • successLogPrintsMeasuredElapsedForTheWholeRoll — pins item 3: asserts the
    success line is Level.INFO and contains elapsedMs=6500.

All three use a self-advancing fake clock (() -> clock.addAndGet(500)), not the
frozen fixedClock, so the measured values are deterministic and provably distinct
from the configured budget — this sidesteps fleetd #486 (a frozen clock + no-op
settleSleeper would otherwise make elapsed always read 0, or hang a test that
expects a real timeout).

Log content is captured with a ch.qos.logback.core.read.ListAppender, following
the existing pattern in AmqpConnectionFailureLoggerTest — no new dependency added
to any pom.xml.

Build

cd fleetd && mvn clean install

Tests run: 1680, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS.
LeadRolloverTest alone: Tests run: 32, Failures: 0, Errors: 0, Skipped: 0 (29
original + 3 new).

Final text of each changed log line (as it will appear)

  • Item 1 (failure): lead-rollover: pane {} did not reach a turn boundary (IDLE or DONE) after /clear — NOT sending bootstrapText (token={}, configured={}s elapsed={}ms nudges={})
  • Item 2 (grace release, now WARN): lead-rollover: /clear on {} was never observed as WORKING after {} consecutive IDLE/DONE polls ({} of those were nudged) — releasing rather than wedging the roll (elapsed={}ms)
  • Item 3 (success): lead-rollover: rolled token={} lead={} elapsedMs={}

Out of scope — found, not fixed

The ticket named exactly these three log lines. One more line in the SAME file has
the identical defect shape but was explicitly out of scope for this ticket:
LeadRollover.runRollover's turn-settle timeout warn (~line 386-391): "did not reach a turn boundary (IDLE or DONE) within {}s after confirm()" using
cfg.turnSettleSeconds() — same "configured value printed as if measured" shape,
left untouched per the ticket's scope.

A wider search of the rest of fleetd/src/main/java for the same shape was run in
parallel and is reported separately in the reply (not part of this diff).


Follow-up (commit c87cc25, same branch/PR) — two additions

The lead reviewed the first pass, confirmed the build independently
(Tests run: 1680, Failures: 0, Errors: 0 matching), and asked for two more
changes on this same PR: "Keep everything you have; these are additions."

1. Grace-release line: print the MEASURED nudge count, not the constant

The release line above printed PICKUP_GRACE_POLLS - 1 (a constant) as the nudge
count. It was numerically correct today, but it is exactly the shape this ticket
is about — a fixed value standing in for a measured one — and that same expression
was wrong once before (a mutation test caught it, code review did not).

Changed to print the already-computed nudges field:

lead-rollover: /clear on {} was never observed as WORKING after {} consecutive
IDLE/DONE polls ({} of those were nudged) — releasing rather than wedging the roll
(elapsed={}ms)

now bound to target, PICKUP_GRACE_POLLS, nudges, elapsedMillis (was
..., PICKUP_GRACE_POLLS - 1, elapsedMillis).

Added an assertion to clearGraceReleaseLogIsWarnWithMeasuredElapsed for the
measured nudge count (written as LeadRollover.PICKUP_GRACE_POLLS - 1, not a
hardcoded literal, so it can't pass regardless of which expression is used).

Mutation proof, as required:

  • Temporarily changed PICKUP_GRACE_POLLS from 8 to 5.
  • Confirmed the mutant applied with two greps using different strings:
    grep -n "PICKUP_GRACE_POLLS = 5;" found it; grep -c "static final int PICKUP_GRACE_POLLS = 8" returned 0 (original gone).
  • Ran clearGraceReleaseLogIsWarnWithMeasuredElapsed and read the actual emitted
    line: ...after 5 consecutive IDLE/DONE polls (4 of those were nudged) — releasing rather than wedging the roll (elapsed=3000ms). 4 followed the
    mutation (5 - 1), proving the log now reads the real counter, not a fixed
    formula.
  • Restored PICKUP_GRACE_POLLS to 8, confirmed via grep, and ran the full
    LeadRolloverTest suite as a control: Tests run: 33, Failures: 0, Errors: 0, Skipped: 0 — green.

2. The sibling line at waitUntilAtTurnBoundary (~line 386-391) — now fixed

This is the line the first pass's "out of scope" note above named and left
untouched. The lead's own instruction: "You followed my brief exactly. The brief
was the defect. I named four specific lines when I should have named the shape,
so fix it now."

waitUntilAtTurnBoundary now returns a private TurnSettleResult(settled, elapsedMillis) record (sibling of ClearSettleResult; no nudge count, since this
wait never nudges) instead of a bare boolean, using the same injected nowMillis
clock. The timeout warn now reads:

lead-rollover: pane {} did not reach a turn boundary (IDLE or DONE) after
confirm() — refusing to send /clear at all; the calling lead's own turn is still
live and clearing it now would destroy live context (token={}, configured={}s
elapsed={}ms)

— labelled configured=/elapsed=, no more bare within {}s.

New test turnTimeoutLogPrintsMeasuredElapsedNotJustConfigured (same style as
clearTimeoutLogPrintsMeasuredElapsedAndNudgesNotJustConfigured): asserts the
message contains configured=1s and elapsed=1500ms, and does NOT contain the
bare within 1s phrasing.

Side effect, caught not missed: the new clock read on waitUntilAtTurnBoundary's
success path shifted the shared self-advancing-clock fixture's value in
successLogPrintsMeasuredElapsedForTheWholeRoll from elapsedMs=6500 to
elapsedMs=7000. Re-derived by hand-tracing the new call sequence, updated the
assertion with an explanatory comment. clearTimeoutLogPrintsMeasuredElapsedAndNudgesNotJustConfigured
and clearGraceReleaseLogIsWarnWithMeasuredElapsed are unaffected: waitForClearPickupAndSettle
computes its own fresh startMillis at entry, so its internal timing is
self-contained.

No behaviour change in either item: same sends, same order, same release/refuse
decisions.

Build (follow-up)

LeadRolloverTest alone: Tests run: 33, Failures: 0, Errors: 0, Skipped: 0
(29 original + 3 first-pass + 1 follow-up).
Full cd fleetd && mvn clean install: Tests run: 1681, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS.

Test changes (follow-up)

  • Updated clearGraceReleaseLogIsWarnWithMeasuredElapsed — added the measured
    nudge-count assertion.
  • Updated successLogPrintsMeasuredElapsedForTheWholeRoll — corrected expected
    value elapsedMs=6500 → elapsedMs=7000, with a comment explaining the shift.
  • Added turnTimeoutLogPrintsMeasuredElapsedNotJustConfigured.

Commit c87cc25 pushed to worker/494-1015ce-2, updating this same PR.


Second follow-up (commit e966cba, same branch/PR)

The lead verified the first follow-up independently (Tests run: 1680 → 1681
after their own re-check, matching), then reported three more things measured
in a detached worktree, all inside the same grace-release line
(LeadRollover.java — the /clear on {} was never observed as WORKING...
warn).

Item 1 — the same line still printed one constant

The line reads (before this commit):

log.warn("... after {} consecutive IDLE/DONE polls ({} of those were nudged) ...",
        target, PICKUP_GRACE_POLLS, nudges, elapsedMillis);

The second argument — the poll count — was still PICKUP_GRACE_POLLS, a
constant, even though the loop already tracks the real count in
idlePollsAwaitingPickup (declared, incremented once per iteration, and
compared against the threshold right where this line fires). Same defect
shape as the nudges fix in the prior commit, one argument over. Changed to
print idlePollsAwaitingPickup.

Item 2 — the test's own expectation was built the same way as the bug

clearGraceReleaseLogIsWarnWithMeasuredElapsed asserted:

assertTrue(message.contains("(" + (LeadRollover.PICKUP_GRACE_POLLS - 1) + " of those were nudged)"), ...);

This expectation is computed the same way the buggy production code used to
compute the printed value — from PICKUP_GRACE_POLLS - 1 — so it cannot tell
a fixed constant apart from the real counter. The lead proved this directly:
reverting the nudges fix and re-running gave 33/0 green, i.e. the suite
stayed clean with the defect back in place.

Rewritten to a plain literal, with a comment explaining why it must stay a
literal:

assertTrue(message.contains("after 8 consecutive IDLE/DONE polls (7 of those were nudged)"), ...);

Re-proof of M1 (the lead's own mutation — revert just the nudges argument
back to PICKUP_GRACE_POLLS - 1), after this rewrite:

  • Grep proof mutant applied: grep -n "idlePollsAwaitingPickup, PICKUP_GRACE_POLLS - 1, elapsedMillis" found line 623.
  • Grep proof original gone: grep -c "idlePollsAwaitingPickup, nudges, elapsedMillis" → 0.
  • Result: mvn -Dtest=LeadRolloverTest test → Tests run: 33, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS — still 0 failures.

This is the honest result, and it does not match "at least 1 failure" the way
the brief expected. Reason, verified by re-reading the loop: nudges only
increments in the finally after a submit() attempt, which only runs when
idlePollsAwaitingPickup < PICKUP_GRACE_POLLS on the check just above it —
so by the time this line executes, idlePollsAwaitingPickup has just
reached PICKUP_GRACE_POLLS, and nudges has been incremented exactly
PICKUP_GRACE_POLLS - 1 times. That is a strict loop invariant, not a
coincidence of today's numbers — no possible run of this loop can make
nudges differ from PICKUP_GRACE_POLLS - 1 at this exact call site. So M1,
as literally specified (only the nudges argument), is an equivalent
mutant
here: no assertion on the printed value — literal or derived — can
ever catch it on this line.

To give the literal-based rewrite (Item 2) a mutation it actually can
catch — the one its own comment promises ("if PICKUP_GRACE_POLLS ever
changes, this test must fail") — I additionally mutated the constant's
value, not the log line:

  • grep -n "PICKUP_GRACE_POLLS = 5;" found the mutant; grep -c "PICKUP_GRACE_POLLS = 8;" → 0.
  • Result: mvn -Dtest=LeadRolloverTest test → Tests run: 33, Failures: 2, Errors: 0 — clearGraceReleaseLogIsWarnWithMeasuredElapsed and successLogPrintsMeasuredElapsedForTheWholeRoll both failed (elapsed-time assertions, since fewer polls means less elapsed time under the fixture's advancing clock). The actual printed line under this mutant read "...after 5 consecutive IDLE/DONE polls (4 of those were nudged)...", confirmed by direct inspection to NOT contain my literal "after 8 consecutive IDLE/DONE polls (7 of those were nudged)" — so the new assertion would also fail here, independent of which assertion in the test throws first.
  • Restored: grep -n "PICKUP_GRACE_POLLS = 8;" confirmed back; control run: Tests run: 33, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.

Item 3 — corrected the comment to state the honest position

The old comment implied a mutation test "caught" the constant standing in for
the counter on this line. Rewritten to say what's actually true: both
counters have exactly one write site on this loop's release branch, so they
cannot differ from the constants at this call site — no test can prove the
difference here, printing them buys one source of truth for the loop (not a
provable improvement by test), and the place nudges genuinely varies with
the run and is covered by a discriminating test is the /clear-timeout
warn's clearResult.nudges() in runRollover.

Build

LeadRolloverTest: Tests run: 33, Failures: 0, Errors: 0, Skipped: 0.
Full mvn clean install: Tests run: 1681, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS.

Same-shape sweep (found, not fixed, per standing instructions)

Injector.java:383-387 — the readiness-grace warn prints the constant
READINESS_GRACE_POLLS as the poll count, where the measured per-target
counter t.notReadySincePoll is in scope (single increment site, at the same
call site it has just reached the threshold — the same equivalent-mutant
situation as the fix above). Not touched.

Commit e966cba pushed to worker/494-1015ce-2, updating this same PR.

## fleetd #494 — log the MEASURED elapsed time, never the configured budget **Goal:** when a lead-session roll goes wrong, the daemon log must print the time it actually waited, never the time it was configured to wait. ### What changed (one file of production code + its test) `fleetd/src/main/java/dev/ltms/fleet/lead/LeadRollover.java`: 1. `runRollover`'s `/clear`-timeout `log.warn` (was ~line 400) now prints `configured=<Ns>s elapsed=<M>ms nudges=<K>`, each labelled, instead of presenting `cfg.clearSettleSeconds()` as the wait duration. 2. `waitForClearPickupAndSettle`'s pickup-grace release (was ~line 569) is now `log.warn` (was `log.info`) and prints the measured elapsed time next to the target pane. The release itself (returning "settled" rather than wedging the roll) is unchanged — only the log level and content. 3. The success line `lead-rollover: rolled` (was ~line 406) now prints the measured elapsed time for the whole roll. 4. `waitForClearPickupAndSettle` now returns a private `ClearSettleResult(settled, elapsedMillis, nudges)` record instead of a bare `boolean`, computed from the already-injected `nowMillis` clock — no new field, no new constructor parameter. **No behaviour change**: same sends, same order, same release/refuse decisions — only what gets logged changed. All 29 original `LeadRolloverTest` tests still pass unmodified. ### Tests added (3, one per changed line, in `LeadRolloverTest.java`) - `clearTimeoutLogPrintsMeasuredElapsedAndNudgesNotJustConfigured` — pins item 1: asserts the WARN line contains `configured=1s`, `elapsed=1500ms`, `nudges=0` and does NOT contain `within 1s`. - `clearGraceReleaseLogIsWarnWithMeasuredElapsed` — pins item 2: asserts the release line is `Level.WARN` (not INFO) and contains `elapsed=4500ms` plus the target pane. - `successLogPrintsMeasuredElapsedForTheWholeRoll` — pins item 3: asserts the success line is `Level.INFO` and contains `elapsedMs=6500`. All three use a **self-advancing fake clock** (`() -> clock.addAndGet(500)`), not the frozen `fixedClock`, so the measured values are deterministic and provably distinct from the configured budget — this sidesteps fleetd #486 (a frozen clock + no-op `settleSleeper` would otherwise make elapsed always read 0, or hang a test that expects a real timeout). Log content is captured with a `ch.qos.logback.core.read.ListAppender`, following the existing pattern in `AmqpConnectionFailureLoggerTest` — no new dependency added to any `pom.xml`. ### Build ``` cd fleetd && mvn clean install ``` `Tests run: 1680, Failures: 0, Errors: 0, Skipped: 0` — `BUILD SUCCESS`. `LeadRolloverTest` alone: `Tests run: 32, Failures: 0, Errors: 0, Skipped: 0` (29 original + 3 new). ### Final text of each changed log line (as it will appear) - Item 1 (failure): `lead-rollover: pane {} did not reach a turn boundary (IDLE or DONE) after /clear — NOT sending bootstrapText (token={}, configured={}s elapsed={}ms nudges={})` - Item 2 (grace release, now WARN): `lead-rollover: /clear on {} was never observed as WORKING after {} consecutive IDLE/DONE polls ({} of those were nudged) — releasing rather than wedging the roll (elapsed={}ms)` - Item 3 (success): `lead-rollover: rolled token={} lead={} elapsedMs={}` ### Out of scope — found, not fixed The ticket named exactly these three log lines. One more line in the SAME file has the identical defect shape but was explicitly out of scope for this ticket: `LeadRollover.runRollover`'s turn-settle timeout warn (~line 386-391): `"did not reach a turn boundary (IDLE or DONE) within {}s after confirm()"` using `cfg.turnSettleSeconds()` — same "configured value printed as if measured" shape, left untouched per the ticket's scope. A wider search of the rest of `fleetd/src/main/java` for the same shape was run in parallel and is reported separately in the reply (not part of this diff). --- ## Follow-up (commit c87cc25, same branch/PR) — two additions The lead reviewed the first pass, confirmed the build independently (`Tests run: 1680, Failures: 0, Errors: 0` matching), and asked for two more changes on this same PR: "Keep everything you have; these are additions." ### 1. Grace-release line: print the MEASURED nudge count, not the constant The release line above printed `PICKUP_GRACE_POLLS - 1` (a constant) as the nudge count. It was numerically correct today, but it is exactly the shape this ticket is about — a fixed value standing in for a measured one — and that same expression was wrong once before (a mutation test caught it, code review did not). Changed to print the already-computed `nudges` field: ``` lead-rollover: /clear on {} was never observed as WORKING after {} consecutive IDLE/DONE polls ({} of those were nudged) — releasing rather than wedging the roll (elapsed={}ms) ``` now bound to `target, PICKUP_GRACE_POLLS, nudges, elapsedMillis` (was `..., PICKUP_GRACE_POLLS - 1, elapsedMillis`). Added an assertion to `clearGraceReleaseLogIsWarnWithMeasuredElapsed` for the measured nudge count (written as `LeadRollover.PICKUP_GRACE_POLLS - 1`, not a hardcoded literal, so it can't pass regardless of which expression is used). **Mutation proof, as required:** - Temporarily changed `PICKUP_GRACE_POLLS` from `8` to `5`. - Confirmed the mutant applied with two greps using different strings: `grep -n "PICKUP_GRACE_POLLS = 5;"` found it; `grep -c "static final int PICKUP_GRACE_POLLS = 8"` returned `0` (original gone). - Ran `clearGraceReleaseLogIsWarnWithMeasuredElapsed` and read the actual emitted line: `...after 5 consecutive IDLE/DONE polls (4 of those were nudged) — releasing rather than wedging the roll (elapsed=3000ms)`. `4` followed the mutation (`5 - 1`), proving the log now reads the real counter, not a fixed formula. - Restored `PICKUP_GRACE_POLLS` to `8`, confirmed via grep, and ran the full `LeadRolloverTest` suite as a control: `Tests run: 33, Failures: 0, Errors: 0, Skipped: 0` — green. ### 2. The sibling line at `waitUntilAtTurnBoundary` (~line 386-391) — now fixed This is the line the first pass's "out of scope" note above named and left untouched. The lead's own instruction: "You followed my brief exactly. The brief was the defect. I named four specific lines when I should have named the shape, so fix it now." `waitUntilAtTurnBoundary` now returns a private `TurnSettleResult(settled, elapsedMillis)` record (sibling of `ClearSettleResult`; no nudge count, since this wait never nudges) instead of a bare `boolean`, using the same injected `nowMillis` clock. The timeout warn now reads: ``` lead-rollover: pane {} did not reach a turn boundary (IDLE or DONE) after confirm() — refusing to send /clear at all; the calling lead's own turn is still live and clearing it now would destroy live context (token={}, configured={}s elapsed={}ms) ``` — labelled `configured=`/`elapsed=`, no more bare `within {}s`. New test `turnTimeoutLogPrintsMeasuredElapsedNotJustConfigured` (same style as `clearTimeoutLogPrintsMeasuredElapsedAndNudgesNotJustConfigured`): asserts the message contains `configured=1s` and `elapsed=1500ms`, and does NOT contain the bare `within 1s` phrasing. **Side effect, caught not missed:** the new clock read on `waitUntilAtTurnBoundary`'s success path shifted the shared self-advancing-clock fixture's value in `successLogPrintsMeasuredElapsedForTheWholeRoll` from `elapsedMs=6500` to `elapsedMs=7000`. Re-derived by hand-tracing the new call sequence, updated the assertion with an explanatory comment. `clearTimeoutLogPrintsMeasuredElapsedAndNudgesNotJustConfigured` and `clearGraceReleaseLogIsWarnWithMeasuredElapsed` are unaffected: `waitForClearPickupAndSettle` computes its own fresh `startMillis` at entry, so its internal timing is self-contained. No behaviour change in either item: same sends, same order, same release/refuse decisions. ### Build (follow-up) `LeadRolloverTest` alone: `Tests run: 33, Failures: 0, Errors: 0, Skipped: 0` (29 original + 3 first-pass + 1 follow-up). Full `cd fleetd && mvn clean install`: `Tests run: 1681, Failures: 0, Errors: 0, Skipped: 0` — `BUILD SUCCESS`. ### Test changes (follow-up) - Updated `clearGraceReleaseLogIsWarnWithMeasuredElapsed` — added the measured nudge-count assertion. - Updated `successLogPrintsMeasuredElapsedForTheWholeRoll` — corrected expected value `elapsedMs=6500 → elapsedMs=7000`, with a comment explaining the shift. - Added `turnTimeoutLogPrintsMeasuredElapsedNotJustConfigured`. Commit `c87cc25` pushed to `worker/494-1015ce-2`, updating this same PR. --- ## Second follow-up (commit e966cba, same branch/PR) The lead verified the first follow-up independently (`Tests run: 1680 → 1681` after their own re-check, matching), then reported three more things measured in a detached worktree, all inside the same grace-release line (`LeadRollover.java` — the `/clear on {} was never observed as WORKING...` warn). ### Item 1 — the same line still printed one constant The line reads (before this commit): ```java log.warn("... after {} consecutive IDLE/DONE polls ({} of those were nudged) ...", target, PICKUP_GRACE_POLLS, nudges, elapsedMillis); ``` The **second** argument — the poll count — was still `PICKUP_GRACE_POLLS`, a constant, even though the loop already tracks the real count in `idlePollsAwaitingPickup` (declared, incremented once per iteration, and compared against the threshold right where this line fires). Same defect shape as the `nudges` fix in the prior commit, one argument over. Changed to print `idlePollsAwaitingPickup`. ### Item 2 — the test's own expectation was built the same way as the bug `clearGraceReleaseLogIsWarnWithMeasuredElapsed` asserted: ```java assertTrue(message.contains("(" + (LeadRollover.PICKUP_GRACE_POLLS - 1) + " of those were nudged)"), ...); ``` This expectation is *computed the same way* the buggy production code used to compute the printed value — from `PICKUP_GRACE_POLLS - 1` — so it cannot tell a fixed constant apart from the real counter. The lead proved this directly: reverting the nudges fix and re-running gave `33/0` green, i.e. the suite stayed clean with the defect back in place. Rewritten to a plain literal, with a comment explaining why it must stay a literal: ```java assertTrue(message.contains("after 8 consecutive IDLE/DONE polls (7 of those were nudged)"), ...); ``` **Re-proof of M1 (the lead's own mutation — revert just the `nudges` argument back to `PICKUP_GRACE_POLLS - 1`), after this rewrite:** - Grep proof mutant applied: `grep -n "idlePollsAwaitingPickup, PICKUP_GRACE_POLLS - 1, elapsedMillis"` found line 623. - Grep proof original gone: `grep -c "idlePollsAwaitingPickup, nudges, elapsedMillis"` → `0`. - Result: `mvn -Dtest=LeadRolloverTest test` → **`Tests run: 33, Failures: 0, Errors: 0, Skipped: 0`, BUILD SUCCESS — still 0 failures.** This is the honest result, and it does not match "at least 1 failure" the way the brief expected. Reason, verified by re-reading the loop: `nudges` only increments in the `finally` after a `submit()` attempt, which only runs when `idlePollsAwaitingPickup < PICKUP_GRACE_POLLS` on the check just above it — so by the time this line executes, `idlePollsAwaitingPickup` has *just* reached `PICKUP_GRACE_POLLS`, and `nudges` has been incremented exactly `PICKUP_GRACE_POLLS - 1` times. That is a strict loop invariant, not a coincidence of today's numbers — no possible run of this loop can make `nudges` differ from `PICKUP_GRACE_POLLS - 1` at this exact call site. So M1, as literally specified (only the `nudges` argument), is an **equivalent mutant** here: no assertion on the printed value — literal or derived — can ever catch it on this line. To give the literal-based rewrite (Item 2) a mutation it actually **can** catch — the one its own comment promises ("if PICKUP_GRACE_POLLS ever changes, this test must fail") — I additionally mutated the constant's *value*, not the log line: - `grep -n "PICKUP_GRACE_POLLS = 5;"` found the mutant; `grep -c "PICKUP_GRACE_POLLS = 8;"` → `0`. - Result: `mvn -Dtest=LeadRolloverTest test` → **`Tests run: 33, Failures: 2, Errors: 0`** — `clearGraceReleaseLogIsWarnWithMeasuredElapsed` and `successLogPrintsMeasuredElapsedForTheWholeRoll` both failed (elapsed-time assertions, since fewer polls means less elapsed time under the fixture's advancing clock). The actual printed line under this mutant read `"...after 5 consecutive IDLE/DONE polls (4 of those were nudged)..."`, confirmed by direct inspection to NOT contain my literal `"after 8 consecutive IDLE/DONE polls (7 of those were nudged)"` — so the new assertion would also fail here, independent of which assertion in the test throws first. - Restored: `grep -n "PICKUP_GRACE_POLLS = 8;"` confirmed back; **control run: `Tests run: 33, Failures: 0, Errors: 0, Skipped: 0`, BUILD SUCCESS.** ### Item 3 — corrected the comment to state the honest position The old comment implied a mutation test "caught" the constant standing in for the counter on this line. Rewritten to say what's actually true: both counters have exactly one write site on this loop's release branch, so they cannot differ from the constants at this call site — no test can prove the difference here, printing them buys one source of truth for the loop (not a provable improvement by test), and the place `nudges` genuinely varies with the run *and* is covered by a discriminating test is the `/clear`-timeout warn's `clearResult.nudges()` in `runRollover`. ### Build `LeadRolloverTest`: `Tests run: 33, Failures: 0, Errors: 0, Skipped: 0`. Full `mvn clean install`: `Tests run: 1681, Failures: 0, Errors: 0, Skipped: 0` — `BUILD SUCCESS`. ### Same-shape sweep (found, not fixed, per standing instructions) `Injector.java:383-387` — the readiness-grace warn prints the constant `READINESS_GRACE_POLLS` as the poll count, where the measured per-target counter `t.notReadySincePoll` is in scope (single increment site, at the same call site it has just reached the threshold — the same equivalent-mutant situation as the fix above). Not touched. Commit `e966cba` pushed to `worker/494-1015ce-2`, updating this same PR.
agent added 1 commit 2026-09-12 04:08:37 +02:00
fleetd #494: log measured elapsed time, never the configured budget, on a lead-rollover failure/success
CI / contract (pull_request) Successful in 52s
CI / build (pull_request) Successful in 1m35s
3fb331145a
- runRollover's /clear-timeout warn now prints configured/elapsed/nudges, each labelled,
  instead of presenting cfg.clearSettleSeconds() as if it were the measured wait.
- waitForClearPickupAndSettle's pickup-grace release is now log.warn (was log.info) and
  prints the measured elapsed time next to the target pane — this is the exact path that
  reported a false-success roll in the real incident (438ms of a 20s budget).
- The success line ('lead-rollover: rolled') now prints the measured elapsed time for the
  whole roll.
- waitForClearPickupAndSettle now returns a ClearSettleResult(settled, elapsedMillis, nudges)
  instead of a bare boolean, so callers can log the measured values instead of the config.
- No behaviour change: same sends, same order, same release/refuse decisions.
- Adds 3 tests to LeadRolloverTest pinning the content of each changed log line, using a
  self-advancing fake clock so the measured elapsed/nudge values are deterministic and
  provably distinct from the configured budget.
agent added 1 commit 2026-09-12 04:22:27 +02:00
fleetd #494 follow-up: print the measured nudge count, and fix the sibling turn-settle timeout line
CI / contract (pull_request) Successful in 1m13s
CI / build (pull_request) Successful in 1m30s
c87cc25aa6
- waitForClearPickupAndSettle's grace-release warn now prints the measured 'nudges' counter
  instead of the constant PICKUP_GRACE_POLLS - 1. The two happen to agree today, but the
  constant expression was wrong once before (printed PICKUP_GRACE_POLLS itself, claiming 8
  nudges where 7 went out) and a code read did not catch it — only a mutation test did.
  Printing the counter cannot drift from the loop's real behaviour.
- waitUntilAtTurnBoundary (the FIRST wait, ~line 386-391) had the identical 'configured value
  printed as if measured' defect as the three lines fixed in the original #494 commit, but was
  out of scope because the brief named specific lines instead of the shape. Fixed the same way:
  it now returns a TurnSettleResult(settled, elapsedMillis) instead of a bare boolean, and the
  timeout warn prints 'configured={}s elapsed={}ms' instead of presenting cfg.turnSettleSeconds()
  as the measured wait.
- No behaviour change: same sends, same order, same release/refuse decisions.
- Test additions: clearGraceReleaseLogIsWarnWithMeasuredElapsed now also asserts the measured
  nudge count; successLogPrintsMeasuredElapsedForTheWholeRoll's expected elapsed value is
  updated (7000ms, not 6500ms) to account for waitUntilAtTurnBoundary's own new clock read; a
  new turnTimeoutLogPrintsMeasuredElapsedNotJustConfigured test pins the sibling line.
- Proved the nudges fix with a temporary mutation: set PICKUP_GRACE_POLLS to 5, confirmed via
  two greps that the mutant applied and the original constant was gone, ran the grace-release
  test and read the actual log line — nudge count followed to 4 (= 5 - 1), then restored to 8
  and reran the full LeadRolloverTest suite as a control (33/33 green).
Author
Member

Follow-up commit c87cc25 — two more items on the same PR

1. Grace-release line: print the measured nudges, not the constant

waitForClearPickupAndSettle's grace-limit release warn used to print
PICKUP_GRACE_POLLS - 1 (a constant expression) as if it were "how many nudges
went out". It was numerically right today, but that exact expression was wrong
once before this ticket (it printed PICKUP_GRACE_POLLS itself, claiming 8
nudges where 7 went out) and a code read missed it — only a mutation test caught
it. Now it prints the measured nudges counter instead, so the number can never
drift from the loop's real behavior again, whatever happens to the constant or
the loop later.

Proof (temporary mutation, reverted before commit):

  • Set PICKUP_GRACE_POLLS from 8 to 5.
  • Confirmed the mutant applied with two greps using different search strings:
    grep -n "PICKUP_GRACE_POLLS = 5;" found it; grep -c "static final int PICKUP_GRACE_POLLS = 8" returned 0.
  • Ran clearGraceReleaseLogIsWarnWithMeasuredElapsed and read the actual emitted
    log line: ...never observed as WORKING after 5 consecutive IDLE/DONE polls (4 of those were nudged) — releasing rather than wedging the roll (elapsed=3000ms).
    The nudge count followed the mutation to 4 (= 5 - 1), proving it now comes
    from the real counter, not a hardcoded value.
  • Restored PICKUP_GRACE_POLLS to 8 and reran LeadRolloverTest as a control:
    Tests run: 33, Failures: 0, Errors: 0, Skipped: 0 — green again.

Added an assertion to clearGraceReleaseLogIsWarnWithMeasuredElapsed that the
message contains the measured nudge count (referenced as
LeadRollover.PICKUP_GRACE_POLLS - 1 in the test, not a hardcoded 7, so the
assertion itself doesn't silently stop discriminating if the constant changes).

2. The sibling line at waitUntilAtTurnBoundary — fixed

The original brief named four specific lines and left out
"...did not reach a turn boundary (IDLE or DONE) within {}s after confirm()"
(the FIRST wait's timeout warn, ~line 386-391), even though it has the identical
"configured value printed as if measured" defect. Fixed the same way as the
/clear wait:

  • waitUntilAtTurnBoundary now returns a private TurnSettleResult(settled, elapsedMillis) record instead of a bare boolean (sibling of
    ClearSettleResult, minus the nudge count since this wait never nudges).
  • The timeout warn now reads:
    lead-rollover: pane {} did not reach a turn boundary (IDLE or DONE) after confirm() — refusing to send /clear at all; the calling lead's own turn is still live and clearing it now would destroy live context (token={}, configured={}s elapsed={}ms)
    

New test: turnTimeoutLogPrintsMeasuredElapsedNotJustConfigured — asserts the
WARN message contains configured=1s and elapsed=1500ms, and does NOT contain
the bare within 1s phrasing.

Side effect on an existing test

waitUntilAtTurnBoundary now reads the injected clock one extra time (to
compute its own measured elapsed on the success path). This shifted
successLogPrintsMeasuredElapsedForTheWholeRoll's deterministic fixture value
from elapsedMs=6500 to elapsedMs=7000 — updated the assertion and added a
comment explaining why.

Build

cd fleetd && mvn clean install

Tests run: 1681, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS.
LeadRolloverTest alone: Tests run: 33, Failures: 0, Errors: 0, Skipped: 0
(29 original + 4 new: 3 from the first commit, 1 new in this follow-up).

No behavior change in either item: same sends, same order, same release/refuse
decisions.

## Follow-up commit `c87cc25` — two more items on the same PR ### 1. Grace-release line: print the measured `nudges`, not the constant `waitForClearPickupAndSettle`'s grace-limit release warn used to print `PICKUP_GRACE_POLLS - 1` (a constant expression) as if it were "how many nudges went out". It was numerically right today, but that exact expression was wrong once before this ticket (it printed `PICKUP_GRACE_POLLS` itself, claiming 8 nudges where 7 went out) and a code read missed it — only a mutation test caught it. Now it prints the measured `nudges` counter instead, so the number can never drift from the loop's real behavior again, whatever happens to the constant or the loop later. **Proof (temporary mutation, reverted before commit):** - Set `PICKUP_GRACE_POLLS` from `8` to `5`. - Confirmed the mutant applied with two greps using different search strings: `grep -n "PICKUP_GRACE_POLLS = 5;"` found it; `grep -c "static final int PICKUP_GRACE_POLLS = 8"` returned `0`. - Ran `clearGraceReleaseLogIsWarnWithMeasuredElapsed` and read the actual emitted log line: `...never observed as WORKING after 5 consecutive IDLE/DONE polls (4 of those were nudged) — releasing rather than wedging the roll (elapsed=3000ms)`. The nudge count followed the mutation to `4` (`= 5 - 1`), proving it now comes from the real counter, not a hardcoded value. - Restored `PICKUP_GRACE_POLLS` to `8` and reran `LeadRolloverTest` as a control: `Tests run: 33, Failures: 0, Errors: 0, Skipped: 0` — green again. Added an assertion to `clearGraceReleaseLogIsWarnWithMeasuredElapsed` that the message contains the measured nudge count (referenced as `LeadRollover.PICKUP_GRACE_POLLS - 1` in the test, not a hardcoded `7`, so the assertion itself doesn't silently stop discriminating if the constant changes). ### 2. The sibling line at `waitUntilAtTurnBoundary` — fixed The original brief named four specific lines and left out `"...did not reach a turn boundary (IDLE or DONE) within {}s after confirm()"` (the FIRST wait's timeout warn, ~line 386-391), even though it has the identical "configured value printed as if measured" defect. Fixed the same way as the `/clear` wait: - `waitUntilAtTurnBoundary` now returns a private `TurnSettleResult(settled, elapsedMillis)` record instead of a bare `boolean` (sibling of `ClearSettleResult`, minus the nudge count since this wait never nudges). - The timeout warn now reads: ``` lead-rollover: pane {} did not reach a turn boundary (IDLE or DONE) after confirm() — refusing to send /clear at all; the calling lead's own turn is still live and clearing it now would destroy live context (token={}, configured={}s elapsed={}ms) ``` New test: `turnTimeoutLogPrintsMeasuredElapsedNotJustConfigured` — asserts the WARN message contains `configured=1s` and `elapsed=1500ms`, and does NOT contain the bare `within 1s` phrasing. ### Side effect on an existing test `waitUntilAtTurnBoundary` now reads the injected clock one extra time (to compute its own measured elapsed on the success path). This shifted `successLogPrintsMeasuredElapsedForTheWholeRoll`'s deterministic fixture value from `elapsedMs=6500` to `elapsedMs=7000` — updated the assertion and added a comment explaining why. ### Build ``` cd fleetd && mvn clean install ``` `Tests run: 1681, Failures: 0, Errors: 0, Skipped: 0` — `BUILD SUCCESS`. `LeadRolloverTest` alone: `Tests run: 33, Failures: 0, Errors: 0, Skipped: 0` (29 original + 4 new: 3 from the first commit, 1 new in this follow-up). No behavior change in either item: same sends, same order, same release/refuse decisions.
Owner

Lead verification of c87cc25 — build green, but the new test does not pin the new fix

Verified by me in a detached worktree at c87cc25, not taken from the worker's report.

mvn clean install
  Tests run: 1681, Failures: 0, Errors: 0, Skipped: 0
  BUILD SUCCESS
  LeadRolloverTest: Tests run: 33, Failures: 0, Errors: 0, Skipped: 0

The code change is right. Two things are not, and I measured both.

Mutation M1 — the fix is not pinned

I reverted exactly the change c87cc25 made to the grace-release warn:

target, PICKUP_GRACE_POLLS, nudges, elapsedMillis);                   // c87cc25
target, PICKUP_GRACE_POLLS, PICKUP_GRACE_POLLS - 1, elapsedMillis);   // M1 (the old defect)

Mutant proved applied with two greps using different search strings: grep -n 'PICKUP_GRACE_POLLS, PICKUP_GRACE_POLLS - 1, elapsedMillis' found LeadRollover.java:619; grep -c 'nudges, elapsedMillis' returned 0.

Result: Tests run: 33, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS. The suite is green with the defect back in.

Harness proof, so that zero is a real negative and not a test that never ran: changing the log text of those were nudged to of those were POKED gave 1 failure, LeadRolloverTest.clearGraceReleaseLogIsWarnWithMeasuredElapsed:980. That test does run and can go red.

Control after restore: 33/0 green, and shasum -a 256 on LeadRollover.java byte-identical to the pristine file.

Cause: LeadRolloverTest.java:980 builds its expected value from LeadRollover.PICKUP_GRACE_POLLS - 1 — the exact expression the fix removed. The test and the code derive the number the same way, so the test cannot see the change.

The same line still prints one constant

LeadRollover.java:616-619, second argument, is still PICKUP_GRACE_POLLS. The measured counter idlePollsAwaitingPickup is in scope; grep -n 'idlePollsAwaitingPickup' returns exactly two lines — :582 declares it, :600 increments it. Same defect as the one this commit fixed, on the same line, one argument to the left.

The honest limit, which the comment overstates

grep -n 'nudges' shows nudges is written in exactly one place, :628, inside the same branch that :600 increments. So on this branch both counters equal the constants by construction — no input can make them differ. No test can prove the difference here, including after the test above is fixed; a literal assertion only catches somebody editing PICKUP_GRACE_POLLS.

The value of the change is that the line gets one source of truth instead of two, so a later edit to the loop cannot leave the message behind. The comment at :608-615 should say that, and should not imply a test now covers it. The place nudges genuinely varies with the run is the /clear-timeout warn at :409-413, which prints clearResult.nudges() — that one is a real measurement, and it is the part of this PR that matters most.

Status

Not merging until the follow-up lands. Follow-up brief is out to the same worker with all of the above attached.

## Lead verification of `c87cc25` — build green, but the new test does not pin the new fix Verified by me in a detached worktree at `c87cc25`, not taken from the worker's report. ``` mvn clean install Tests run: 1681, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS LeadRolloverTest: Tests run: 33, Failures: 0, Errors: 0, Skipped: 0 ``` The code change is right. Two things are not, and I measured both. ### Mutation M1 — the fix is not pinned I reverted exactly the change `c87cc25` made to the grace-release warn: ```java target, PICKUP_GRACE_POLLS, nudges, elapsedMillis); // c87cc25 target, PICKUP_GRACE_POLLS, PICKUP_GRACE_POLLS - 1, elapsedMillis); // M1 (the old defect) ``` Mutant proved applied with two greps using different search strings: `grep -n 'PICKUP_GRACE_POLLS, PICKUP_GRACE_POLLS - 1, elapsedMillis'` found `LeadRollover.java:619`; `grep -c 'nudges, elapsedMillis'` returned `0`. **Result: `Tests run: 33, Failures: 0, Errors: 0, Skipped: 0`, BUILD SUCCESS.** The suite is green with the defect back in. Harness proof, so that zero is a real negative and not a test that never ran: changing the log text `of those were nudged` to `of those were POKED` gave 1 failure, `LeadRolloverTest.clearGraceReleaseLogIsWarnWithMeasuredElapsed:980`. That test does run and can go red. Control after restore: 33/0 green, and `shasum -a 256` on `LeadRollover.java` byte-identical to the pristine file. Cause: `LeadRolloverTest.java:980` builds its expected value from `LeadRollover.PICKUP_GRACE_POLLS - 1` — the exact expression the fix removed. The test and the code derive the number the same way, so the test cannot see the change. ### The same line still prints one constant `LeadRollover.java:616-619`, second argument, is still `PICKUP_GRACE_POLLS`. The measured counter `idlePollsAwaitingPickup` is in scope; `grep -n 'idlePollsAwaitingPickup'` returns exactly two lines — `:582` declares it, `:600` increments it. Same defect as the one this commit fixed, on the same line, one argument to the left. ### The honest limit, which the comment overstates `grep -n 'nudges'` shows `nudges` is written in exactly one place, `:628`, inside the same branch that `:600` increments. So on this branch both counters equal the constants **by construction** — no input can make them differ. No test can prove the difference here, including after the test above is fixed; a literal assertion only catches somebody editing `PICKUP_GRACE_POLLS`. The value of the change is that the line gets one source of truth instead of two, so a later edit to the loop cannot leave the message behind. The comment at `:608-615` should say that, and should not imply a test now covers it. The place `nudges` genuinely varies with the run is the `/clear`-timeout warn at `:409-413`, which prints `clearResult.nudges()` — that one is a real measurement, and it is the part of this PR that matters most. ### Status Not merging until the follow-up lands. Follow-up brief is out to the same worker with all of the above attached.
agent added 1 commit 2026-09-12 04:39:46 +02:00
fleetd #494 follow-up (2nd pass): the grace-release line still had one constant, and its test could not tell the difference
CI / contract (pull_request) Successful in 1m26s
CI / build (pull_request) Successful in 1m55s
e966cbadf9
Two more fixes on the same line, LeadRollover.java:600-624:

1. The poll-count argument (second, was PICKUP_GRACE_POLLS) now prints the
   loop's own idlePollsAwaitingPickup counter instead of the constant. Same
   defect shape as the nudges fix from c87cc25, one argument over.

2. LeadRolloverTest's clearGraceReleaseLogIsWarnWithMeasuredElapsed asserted
   its nudge-count expectation as `LeadRollover.PICKUP_GRACE_POLLS - 1` — the
   same expression the production code used to build the log line from, so it
   could not discriminate a reverted fix. Rewritten to plain literals
   ("after 8 consecutive IDLE/DONE polls (7 of those were nudged)"), proven to
   trip when PICKUP_GRACE_POLLS's value changes (2 failures under a
   PICKUP_GRACE_POLLS=5 mutation: this test and successLogPrintsMeasured...).

Also corrected the comment above the log line: idlePollsAwaitingPickup and
nudges each have exactly one write site on this loop's release branch, so they
cannot differ from PICKUP_GRACE_POLLS / PICKUP_GRACE_POLLS - 1 at this call
site — confirmed by re-deriving the loop's control flow, and by reverting just
the nudges argument (M1) and observing 33/0 stayed green even after the test
rewrite. That is an equivalent mutant on this line, not a gap the test rewrite
could close; the honest value of printing the counters is one source of truth
for the loop, not a provable-by-test difference here. The place nudges truly
varies with the run — and is covered by a test that can tell it apart from a
constant — is the /clear-timeout warn's clearResult.nudges() in runRollover.

mvn -Dtest=LeadRolloverTest test: 33/0. mvn clean install: 1681/0, BUILD SUCCESS.

Same-shape sweep (found, not fixed, per instructions):
Injector.java:383-387 — the readiness-grace warn prints the constant
READINESS_GRACE_POLLS where the measured per-target counter
t.notReadySincePoll is in scope (single increment site, just reached the
threshold at this call site — same equivalent-mutant situation as this fix).
ltms merged commit 8f59019305 into main 2026-09-12 04:44:10 +02:00
Sign in to join this conversation.