Five tests pinning the invariants that decide WHICH turn a reply belongs to. These protect against a silent correctness bug — a reply attributed to the wrong turn — not against a crash, which is why they were worth picking over higher-percentage coverage gaps. Chosen by blast radius, not by uncovered-line count. Both guards are compound conditions with a side that never executed, i.e. exactly the shape where a clause can be deleted as "redundant" and every existing test still passes. CompletionResolver: - The CB-115 misattribution guard suppresses a completion when the scrape is byte-identical to the pane at delivery. Its !scrapeFailed clause was unexercised: delete it and a FAILED read is misread as "no output change", so the send is suppressed and hangs to the caller's timeout instead of resolving. The new test sets the baseline to "" so the empty tail from a failed read would byte-match and wrongly suppress — built to die precisely when that clause dies. - The fail() guard leaves an already-resolved waiter alone. The new test also asserts agent.read is never called, so the worker is not scraped for a send nobody is waiting on. Rendezvous: a second resolution of an already-completed waiter returns false and does not overwrite the first value, for both resolveCompletion and resolveFailure. Verified by sabotage, one guard at a time: removing !scrapeFailed reds resolvesWhenTheScrapeItselfFailsEvenWithABaselinePresent; removing the isDone() clause reds failLeavesAnAlreadyResolvedWaiterUntouchedAndSkipsTheScrape. (The first attempt at the second sabotage reported a false pass — the patch hit an identically-worded guard earlier in the file. Line-targeted and re-run.) 346 tests, was 341. Worker-implemented on the local-vLLM profile; it noticed three of the eight cases I asked for already existed and said so with names rather than duplicating them. Also of note: the first delegation of this ticket wedged the worker — the pane showed a zsh parse error and it went idle with an untouched worktree, task stuck pending. The retry differed only in phrasing the same requirements as prose instead of quoting Java boolean expressions. Filed as a bridge robustness concern: injected content shares a channel with control, and a wedged turn is invisible in both the task view and /metrics.
This commit is contained in:
@@ -195,6 +195,66 @@ class CompletionResolverTest {
|
||||
assertEquals("hello", waiter.getNow(null).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesWhenTheScrapeItselfFailsEvenWithABaselinePresent() {
|
||||
// The most important branch of the CB-115 guard: a failed read means the resolver could not
|
||||
// SEE the screen — "couldn't see", not "no change". It must still resolve the send (an empty
|
||||
// tail beats hanging until the caller's timeout), even though a baseline was captured. The
|
||||
// baseline here is "" (an empty pane at delivery), so without the !scrapeFailed clause the
|
||||
// byte-identical guard would wrongly match the empty tail and suppress.
|
||||
FakeHerdr herdr = new FakeHerdr().healthy(false); // agent.read throws HerdrException
|
||||
Rendezvous rendezvous = new Rendezvous();
|
||||
CompletionResolver resolver = new CompletionResolver(new AgentControl(herdr), rendezvous);
|
||||
|
||||
var waiter = rendezvous.open("term_a");
|
||||
var turn = new CompletionResolver.InFlight(waiter, ""); // empty pane baselined at delivery
|
||||
resolver.resolve("term_a", turn);
|
||||
|
||||
assertTrue(waiter.isDone(),
|
||||
"a failed scrape must still resolve the send, not hang until the caller's timeout");
|
||||
assertEquals(Rendezvous.Kind.COMPLETION, waiter.getNow(null).kind());
|
||||
assertEquals("", waiter.getNow(null).text(), "the tail is empty because the screen was unreadable");
|
||||
}
|
||||
|
||||
// --- CB-115/CB-116 fail guard: an already-done or absent waiter is left alone ---------
|
||||
|
||||
@Test
|
||||
void failLeavesAnAlreadyResolvedWaiterUntouchedAndSkipsTheScrape() {
|
||||
// The send was already resolved (e.g. by the worker's explicit reply) before fail fired.
|
||||
// fail must not overwrite that value, and must not even scrape the worker — nobody needs it.
|
||||
FakeHerdr herdr = new FakeHerdr().readText("an error screen");
|
||||
Rendezvous rendezvous = new Rendezvous();
|
||||
CompletionResolver resolver = new CompletionResolver(new AgentControl(herdr), rendezvous);
|
||||
|
||||
var waiter = rendezvous.open("term_a");
|
||||
var turn = new CompletionResolver.InFlight(waiter, null);
|
||||
assertTrue(rendezvous.resolveCompletion(waiter, "already replied"));
|
||||
|
||||
resolver.fail("term_a", turn);
|
||||
|
||||
assertFalse(herdr.called("agent.read"),
|
||||
"fail must not scrape a waiter that is already done");
|
||||
assertEquals(Rendezvous.Kind.COMPLETION, waiter.getNow(null).kind(),
|
||||
"fail must not overwrite the existing resolution");
|
||||
assertEquals("already replied", waiter.getNow(null).text());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failFallsBackToTheRegisteredWaiterWhenThereIsNoInFlightTurn() {
|
||||
// A never-delivered readiness failure has no in-flight record but still has a blocked send;
|
||||
// fail falls back to the waiter currently registered on the Rendezvous and fails it.
|
||||
FakeHerdr herdr = new FakeHerdr().readText("stuck on an error screen");
|
||||
Rendezvous rendezvous = new Rendezvous();
|
||||
CompletionResolver resolver = new CompletionResolver(new AgentControl(herdr), rendezvous);
|
||||
|
||||
var waiter = rendezvous.open("term_a"); // send registered, but no captureBaseline ever ran
|
||||
resolver.fail("term_a", null); // no in-flight turn → fall back to the registered waiter
|
||||
|
||||
assertTrue(waiter.isDone(), "fail falls back to the registered waiter when no turn is in flight");
|
||||
assertEquals(Rendezvous.Kind.FAILED, waiter.getNow(null).kind());
|
||||
assertEquals("stuck on an error screen", waiter.getNow(null).text());
|
||||
}
|
||||
|
||||
// --- CB-116 waiter identity: a late completion never crosses into the next turn ---------
|
||||
|
||||
@Test
|
||||
|
||||
@@ -70,6 +70,28 @@ class RendezvousTest {
|
||||
"no blocked send means no primary to surface the question to");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveCompletionTwiceIsANoOpTheSecondTime() {
|
||||
CompletableFuture<Rendezvous.Resolution> waiter = rendezvous.open(W);
|
||||
assertTrue(rendezvous.resolveCompletion(waiter, "first scrape"), "the first completion resolves");
|
||||
assertFalse(rendezvous.resolveCompletion(waiter, "second scrape"),
|
||||
"a second completion on an already-resolved waiter returns false");
|
||||
assertEquals(Rendezvous.Kind.COMPLETION, waiter.getNow(null).kind());
|
||||
assertEquals("first scrape", waiter.getNow(null).text(),
|
||||
"the first resolution wins; the stored value is unchanged");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveFailureTwiceIsANoOpTheSecondTime() {
|
||||
CompletableFuture<Rendezvous.Resolution> waiter = rendezvous.open(W);
|
||||
assertTrue(rendezvous.resolveFailure(waiter, "first reason"), "the first failure resolves");
|
||||
assertFalse(rendezvous.resolveFailure(waiter, "second reason"),
|
||||
"a second failure on an already-resolved waiter returns false");
|
||||
assertEquals(Rendezvous.Kind.FAILED, waiter.getNow(null).kind());
|
||||
assertEquals("first reason", waiter.getNow(null).text(),
|
||||
"the first resolution wins; the stored value is unchanged");
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeAskRemovesTheTurn() {
|
||||
Rendezvous.AskTicket t = rendezvous.openAsk(W);
|
||||
|
||||
Reference in New Issue
Block a user