From 1a397e962e2fc154042e0e35256a3c4ded39302a Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sat, 3 Oct 2026 22:06:26 +0200 Subject: [PATCH] fleetd #683: decouple the completion-fallback test's send budget from its own setup clock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit completionFallbackResolvesATurnThatNeverCalledFleetReply gave messages.send a 5000 ms budget that started ticking the instant sendAsync() ran, then raced that same clock against awaitWaiting()'s own 2000 ms deadline plus several onStatus/readText calls before asserting with send.get(5, SECONDS). On a loaded machine the setup could eat enough of the 5000 ms that the production call expired first, returning TIMED_OUT_QUEUED instead of COMPLETED_UNREPLIED. Give this one test's send a 30 000 ms budget (sendAsync(content, timeoutMillis)) so the setup can never compete with it; send.get(5, SECONDS) stays the one clock the test depends on. A new regression test injects a deterministic 5500 ms delay in the same spot and proves the budget is no longer the binding constraint — reverting it to 5000 ms turns that test red with the same TIMED_OUT_QUEUED mismatch, confirmed by mutation. --- .../ltms/fleet/msg/MessageServiceTest.java | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java b/fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java index fb5b7f5a..c8404baa 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java @@ -60,13 +60,25 @@ class MessageServiceTest { inbox.own(T); } + /** + * A send budget large enough that a test's own setup — {@link #awaitWaiting()} plus whatever + * status transitions it drives afterward — can never compete with it for the same clock. A test + * that needs {@code send.get(...)}'s own window to be the only timing bound it depends on uses + * {@link #sendAsync(String, long)} with this value instead of the default 5000 ms. + */ + private static final long GENEROUS_SEND_BUDGET_MILLIS = 30_000; + /** Run {@code send} on a background thread; the current thread drives the worker's turn. */ private CompletableFuture sendAsync() { return sendAsync("do the task"); } private CompletableFuture sendAsync(String content) { - return CompletableFuture.supplyAsync(() -> messages.send(T, content, 5000)); + return sendAsync(content, 5000); + } + + private CompletableFuture sendAsync(String content, long timeoutMillis) { + return CompletableFuture.supplyAsync(() -> messages.send(T, content, timeoutMillis)); } private void awaitWaiting() throws InterruptedException { @@ -80,7 +92,7 @@ class MessageServiceTest { @Test void completionFallbackResolvesATurnThatNeverCalledFleetReply() throws Exception { - CompletableFuture send = sendAsync(); + CompletableFuture send = sendAsync("do the task", GENEROUS_SEND_BUDGET_MILLIS); awaitWaiting(); herdr.readText("$ prompt"); // pre-turn pane: no answer yet (baseline reference) @@ -96,6 +108,32 @@ class MessageServiceTest { assertTrue(reply.completed(), "a scraped completion still counts as completed"); } + /** + * Pins {@link #GENEROUS_SEND_BUDGET_MILLIS} as the budget {@link + * #completionFallbackResolvesATurnThatNeverCalledFleetReply} depends on. A 5500 ms delay between + * {@link #awaitWaiting()} and the status transitions that drive completion stands in for a loaded + * machine's setup overhead — comfortably past the 5000 ms budget this send no longer uses, and + * still well inside this method's own 30 000 ms budget. The only clock this test depends on is + * {@code send.get}'s own 10 s window. + */ + @Test + void completionFallbackSurvivesASlowHarnessBecauseItsSendBudgetIsNotTheBindingClock() throws Exception { + CompletableFuture send = sendAsync("do the task", GENEROUS_SEND_BUDGET_MILLIS); + awaitWaiting(); + + Thread.sleep(5500); + + herdr.readText("$ prompt"); + injector.onStatus(T, AgentStatus.IDLE); + injector.onStatus(T, AgentStatus.WORKING); + herdr.readText("BUILD GREEN: 391 files"); + injector.onStatus(T, AgentStatus.IDLE); + + MessageService.Reply reply = send.get(10, TimeUnit.SECONDS); + assertEquals(MessageService.Outcome.COMPLETED_UNREPLIED, reply.outcome(), + "a slow harness must not be mistaken for a timed-out delivery"); + } + @Test void completionFallbackReplacesAnEchoedInjectedBriefWithNoReportOutcome() throws Exception { String brief = "Implement the requested change. ".repeat(20);