fleetd #489: nudge the /clear submit keystroke before bootstrapText #490

Merged
ltms merged 2 commits from worker/489-001902-2 into main 2026-09-12 02:52:52 +02:00
Member

fleetd #489 — LeadRollover /clear pickup race

Fault 1 (measured live 2026-09-12): runRollover's second wait (after /clear) polled for IDLE/DONE, which /clear itself never leaves — it starts no real turn — so the wait always returned true on the first poll. It was a no-op.

Fault 2: runRollover deliberately bypasses Injector for /clear (to avoid wedging the pane), so it inherits none of Injector's Enter-nudging (CB-113). The submit Enter that accompanies /clear can race the paste and leave it unsubmitted.

Combined: the roll sent /clear then bootstrapText right after, in ~438ms, with no gate in between — landing as one concatenated line (/clearFresh lead session...), answered Unknown command: /clearFresh.

Fix

Replace the second wait with a new private method waitForClearPickupAndSettle, copying the pickup-nudge pattern Injector already ships for its own post-turn /clear housekeeping (fleetd #306, see Injector.java:288-340 and :437-442):

  • a WORKING sample means /clear was picked up as a real turn
  • until that happens, every poll still reporting IDLE/DONE re-sends the submit keystroke (agents.submit), up to PICKUP_GRACE_POLLS = 8 times, then releases rather than wedges (logged at info)
  • once WORKING is observed, nudging stops and it waits for a real WORKING -> IDLE/DONE boundary before returning true
  • BLOCKED is deliberately excluded from both the nudge and the boundary check, same reasoning as the (unchanged) first wait (waitUntilAtTurnBoundary): a paused live turn is not settled, and nudging Enter into an open approval prompt could wrongly answer it
  • a throwing agents.submit is swallowed at debug, same as Injector.java:437-442

The first turnSettleSeconds gate (waitUntilAtTurnBoundary) is unchanged; its javadoc's stale "used twice" sentence is corrected to describe the split.

Tests

Four new tests in LeadRolloverTest:

  1. Pane stays IDLE throughout (the paste-race case) — asserts at least one agent.send_keys nudge happens, in order, between the /clear prompt and the bootstrapText prompt.
  2. A confirmed WORKING pickup followed by IDLE — asserts bootstrapText is sent and no nudge occurred once WORKING was observed.
  3. A status that never reaches a boundary (UNKNOWN the whole time) — asserts bootstrapText is never sent and no nudge occurred.
  4. A throwing submit() — asserts the roll still completes and bootstrapText is still sent.

No changes to FakeHerdr were needed: nudge counting uses its already-exposed calls list, and status-sequence scripting follows the file's existing idiom (a thin HerdrClient wrapper keyed on the /clear prompt call, matching the two tests already in the file — blocksAfterClear/flipsAfterClear).

Verification

  • Baseline (this branch, unmutated): Tests run: 29, Failures: 0 for LeadRolloverTest.
  • Mutation A (deleted the agents.submit(...) nudge call): Tests run: 29, Failures: 1.
  • Mutation B (waitForClearPickupAndSettle returns true immediately, the old no-op): Tests run: 29, Failures: 4.
  • Positive control (tree restored, unmutated): Tests run: 29, Failures: 0, green.
  • Full build: mvn clean install from fleetd/ — Tests run: 1677, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.

Scope: only LeadRollover.java (runRollover's second wait + the new method) and LeadRolloverTest.java, per the ticket.

## fleetd #489 — LeadRollover /clear pickup race **Fault 1 (measured live 2026-09-12):** `runRollover`'s second wait (after `/clear`) polled for IDLE/DONE, which `/clear` itself never leaves — it starts no real turn — so the wait always returned true on the first poll. It was a no-op. **Fault 2:** `runRollover` deliberately bypasses `Injector` for `/clear` (to avoid wedging the pane), so it inherits none of `Injector`'s Enter-nudging (CB-113). The submit Enter that accompanies `/clear` can race the paste and leave it unsubmitted. Combined: the roll sent `/clear` then `bootstrapText` right after, in ~438ms, with no gate in between — landing as one concatenated line (`/clearFresh lead session...`), answered `Unknown command: /clearFresh`. ### Fix Replace the second wait with a new private method `waitForClearPickupAndSettle`, copying the pickup-nudge pattern `Injector` already ships for its own post-turn `/clear` housekeeping (fleetd #306, see `Injector.java:288-340` and `:437-442`): - a `WORKING` sample means `/clear` was picked up as a real turn - until that happens, every poll still reporting IDLE/DONE re-sends the submit keystroke (`agents.submit`), up to `PICKUP_GRACE_POLLS = 8` times, then releases rather than wedges (logged at `info`) - once `WORKING` is observed, nudging stops and it waits for a real `WORKING -> IDLE/DONE` boundary before returning true - `BLOCKED` is deliberately excluded from both the nudge and the boundary check, same reasoning as the (unchanged) first wait (`waitUntilAtTurnBoundary`): a paused live turn is not settled, and nudging Enter into an open approval prompt could wrongly answer it - a throwing `agents.submit` is swallowed at `debug`, same as `Injector.java:437-442` The first `turnSettleSeconds` gate (`waitUntilAtTurnBoundary`) is unchanged; its javadoc's stale "used twice" sentence is corrected to describe the split. ### Tests Four new tests in `LeadRolloverTest`: 1. Pane stays IDLE throughout (the paste-race case) — asserts at least one `agent.send_keys` nudge happens, in order, between the `/clear` prompt and the `bootstrapText` prompt. 2. A confirmed WORKING pickup followed by IDLE — asserts `bootstrapText` is sent and no nudge occurred once WORKING was observed. 3. A status that never reaches a boundary (UNKNOWN the whole time) — asserts `bootstrapText` is never sent and no nudge occurred. 4. A throwing `submit()` — asserts the roll still completes and `bootstrapText` is still sent. No changes to `FakeHerdr` were needed: nudge counting uses its already-exposed `calls` list, and status-sequence scripting follows the file's existing idiom (a thin `HerdrClient` wrapper keyed on the `/clear` prompt call, matching the two tests already in the file — `blocksAfterClear`/`flipsAfterClear`). ### Verification - Baseline (this branch, unmutated): `Tests run: 29, Failures: 0` for `LeadRolloverTest`. - Mutation A (deleted the `agents.submit(...)` nudge call): `Tests run: 29, Failures: 1`. - Mutation B (`waitForClearPickupAndSettle` returns `true` immediately, the old no-op): `Tests run: 29, Failures: 4`. - Positive control (tree restored, unmutated): `Tests run: 29, Failures: 0`, green. - Full build: `mvn clean install` from `fleetd/` — `Tests run: 1677, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`. Scope: only `LeadRollover.java` (`runRollover`'s second wait + the new method) and `LeadRolloverTest.java`, per the ticket.
agent added 1 commit 2026-09-12 02:30:28 +02:00
fleetd #489: nudge the /clear submit keystroke before bootstrapText
CI / contract (pull_request) Successful in 56s
CI / build (pull_request) Successful in 1m36s
c6058652be
LeadRollover.runRollover's second wait (after /clear) was a no-op: it polled
for IDLE/DONE, which /clear itself never leaves since it starts no real turn,
so it always returned true on the first poll. Combined with a direct
agents.send bypassing Injector (deliberate, to avoid wedging the pane), the
submit Enter that accompanies /clear could race the paste and leave it
unsubmitted — bootstrapText then landed concatenated onto the same input
line, exactly as measured live on 2026-09-12.

Replace that second wait with waitForClearPickupAndSettle, which copies the
pickup-nudge pattern Injector already ships for its own post-turn /clear
housekeeping (fleetd #306): nudge agents.submit while the pane hasn't
reported WORKING yet, release after PICKUP_GRACE_POLLS=8 nudges rather than
wedge, and require a real WORKING -> IDLE/DONE boundary once a pickup is
observed. BLOCKED stays excluded from both the nudge and the boundary check,
same as the (unchanged) first wait — a paused live turn is not settled, and
nudging Enter into an open prompt could wrongly answer it.

Adds four tests to LeadRolloverTest covering the paste-race regression
(nudge ordered between /clear and bootstrapText), a confirmed pickup, a
deadline expiry with no boundary ever reached, and a throwing submit().
agent added 1 commit 2026-09-12 02:45:43 +02:00
fleetd #489 follow-up: fix stale class javadoc, off-by-one nudge count, weak test
CI / contract (pull_request) Successful in 1m22s
CI / build (pull_request) Successful in 1m33s
f687046450
Three review corrections on top of the previous commit:

1. The class javadoc's four-step continuation list (lines 47-58) was stale.
   Step 1 said "report an injectable state", but waitUntilAtTurnBoundary's
   own javadoc excludes BLOCKED - fixed to say IDLE or DONE. Step 3 still
   described the old plain re-check ("the original, pre-correction wait...
   still here") - fixed to describe what waitForClearPickupAndSettle
   actually does: nudge while unpicked-up, then wait for a real WORKING ->
   IDLE/DONE boundary, releasing rather than wedging if WORKING never shows.

2. PICKUP_GRACE_POLLS=8 bounds the number of consecutive not-yet-picked-up
   polls, not the number of nudges - the 8th poll releases instead of
   nudging again, so 8 polls produce 7 nudges. The log.info in the release
   branch and two javadoc spots said "8 nudges"; fixed all three to state
   the poll count and the nudge count separately and correctly. Behavior
   and the constant are unchanged.

3. pickupSeenStopsNudgingAndBootstrapTextIsSent asserted only promptCallCount
   and sendKeysCallCount, both of which a return-true stub also satisfies.
   Added an assertion on the already-tracked postClearGetCalls counter
   (>= 2), which only a real post-/clear poll loop can produce - this is
   what makes the test fail against a return-true mutant.
Author
Member

Follow-up: three review corrections, pushed to this branch (f687046)

1. Class javadoc's four-step list corrected

  • Step 1: "report an injectable state" → "report a real turn boundary — IDLE or DONE, never merely BLOCKED" (matches waitUntilAtTurnBoundary's own javadoc, which was already right).
  • Step 3: no longer describes "the original, pre-correction wait — still here"; now describes what waitForClearPickupAndSettle actually does — nudge while no pickup seen, then wait for a real WORKING → IDLE/DONE boundary, release rather than wedge if WORKING never observed.

2. Off-by-one in the nudge count fixed

PICKUP_GRACE_POLLS = 8 bounds polls, not nudges — the 8th poll releases instead of nudging, so 8 polls produce 7 nudges. This was measured (setting the constant to 1 made the paste-race test see zero agent.send_keys calls). Fixed in three places: the constant's own javadoc, the waitForClearPickupAndSettle javadoc bullet, and the log.info in the release branch — all now state poll count and nudge count separately and correctly. Behavior and the constant value (8) are unchanged.

3. Strengthened pickupSeenStopsNudgingAndBootstrapTextIsSent

Added an assertion on the already-tracked postClearGetCalls counter (>= 2) — this is what a return true; stub cannot satisfy, since it never polls status again after /clear. Previously the test only checked promptCallCount and sendKeysCallCount, both of which a stub also passes.

Verification

  • Baseline (corrected tree, unmutated): mvn test -Dtest=LeadRolloverTest → Tests run: 29, Failures: 0, Errors: 0, Skipped: 0.
  • Mutation C — replaced the whole waitForClearPickupAndSettle body with return true;. Proof: grep -n "MUTATION C" found the marker (mutant present); grep -n "idlePollsAwaitingPickup" found nothing, exit 1 (original loop gone). Ran suite: Tests run: 29, Failures: 5. Failing tests:
    • clearPickupIsNudgedBeforeBootstrapTextWhenPaneStaysIdle
    • pickupSeenStopsNudgingAndBootstrapTextIsSent ← now fails, as required (it did not before this follow-up)
    • clearThatNeverSettlesAfterwardsNeverSendsBootstrapText
    • blockedAfterClearNeverSendsBootstrapText
    • clearPickupNeverSettlesWhenStatusNeverReachesABoundary
  • Restored, confirmed byte-identical via diff against the saved corrected copy.
  • Positive control (restored, unmutated): mvn test -Dtest=LeadRolloverTest → Tests run: 29, Failures: 0, Errors: 0, Skipped: 0 — green, so the mutation failures above aren't a broken harness.
  • Full build: mvn clean install from fleetd/ → Tests run: 1677, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.

Scope: only LeadRollover.java (javadoc + log-line text, no behavior change) and LeadRolloverTest.java (one strengthened assertion), per the ticket.

## Follow-up: three review corrections, pushed to this branch (f687046) ### 1. Class javadoc's four-step list corrected - Step 1: "report an injectable state" → "report a real turn boundary — IDLE or DONE, never merely BLOCKED" (matches `waitUntilAtTurnBoundary`'s own javadoc, which was already right). - Step 3: no longer describes "the original, pre-correction wait — still here"; now describes what `waitForClearPickupAndSettle` actually does — nudge while no pickup seen, then wait for a real WORKING → IDLE/DONE boundary, release rather than wedge if WORKING never observed. ### 2. Off-by-one in the nudge count fixed `PICKUP_GRACE_POLLS = 8` bounds *polls*, not *nudges* — the 8th poll releases instead of nudging, so 8 polls produce 7 nudges. This was measured (setting the constant to 1 made the paste-race test see zero `agent.send_keys` calls). Fixed in three places: the constant's own javadoc, the `waitForClearPickupAndSettle` javadoc bullet, and the `log.info` in the release branch — all now state poll count and nudge count separately and correctly. Behavior and the constant value (8) are unchanged. ### 3. Strengthened `pickupSeenStopsNudgingAndBootstrapTextIsSent` Added an assertion on the already-tracked `postClearGetCalls` counter (`>= 2`) — this is what a `return true;` stub cannot satisfy, since it never polls status again after `/clear`. Previously the test only checked `promptCallCount` and `sendKeysCallCount`, both of which a stub also passes. ### Verification - **Baseline** (corrected tree, unmutated): `mvn test -Dtest=LeadRolloverTest` → `Tests run: 29, Failures: 0, Errors: 0, Skipped: 0`. - **Mutation C** — replaced the whole `waitForClearPickupAndSettle` body with `return true;`. Proof: `grep -n "MUTATION C"` found the marker (mutant present); `grep -n "idlePollsAwaitingPickup"` found nothing, exit 1 (original loop gone). Ran suite: `Tests run: 29, Failures: 5`. Failing tests: - `clearPickupIsNudgedBeforeBootstrapTextWhenPaneStaysIdle` - `pickupSeenStopsNudgingAndBootstrapTextIsSent` ← now fails, as required (it did not before this follow-up) - `clearThatNeverSettlesAfterwardsNeverSendsBootstrapText` - `blockedAfterClearNeverSendsBootstrapText` - `clearPickupNeverSettlesWhenStatusNeverReachesABoundary` - **Restored**, confirmed byte-identical via `diff` against the saved corrected copy. - **Positive control** (restored, unmutated): `mvn test -Dtest=LeadRolloverTest` → `Tests run: 29, Failures: 0, Errors: 0, Skipped: 0` — green, so the mutation failures above aren't a broken harness. - **Full build**: `mvn clean install` from `fleetd/` → `Tests run: 1677, Failures: 0, Errors: 0, Skipped: 0`, `BUILD SUCCESS`. Scope: only `LeadRollover.java` (javadoc + log-line text, no behavior change) and `LeadRolloverTest.java` (one strengthened assertion), per the ticket.
ltms merged commit 9f74b6619a into main 2026-09-12 02:52:52 +02:00
Sign in to join this conversation.