diff --git a/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java b/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java index a1787e6..008e472 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java +++ b/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java @@ -613,9 +613,26 @@ public final class CompletionResolver implements TurnListener { return null; } - /** True when the error pattern begins the matched pane line, rather than appearing in prose. */ + /** + * True when the error pattern begins the matched pane line, rather than appearing in prose. + * + *
Leading terminal chrome is skipped first — box-drawing characters, bullets, gutter bars and + * spaces. #339 introduced this check with a bare {@code lookingAt}, and that rejected a genuine + * error line rendered as {@code "| 503 Service Unavailable: ..."}: the send still failed, but the + * credential outage was never recorded. That is the false negative #339's own invariant 3 called + * worse than the false positive it set out to fix — measured with a throwaway probe on the + * raw-scrape path, which is exactly the path whose comment says to expect leading chrome. + * + *
Skipping only a leading run of non-letter, non-digit characters keeps the fix's intent. A + * member's prose ({@code "I checked the retry path. An API Error: makes it back off."}) still + * does not match, because there the pattern sits after words, not after chrome. + */ private static boolean startsWithBackendError(String line, Pattern pattern) { - return pattern.matcher(line).lookingAt(); + int i = 0; + while (i < line.length() && !Character.isLetterOrDigit(line.charAt(i))) { + i++; + } + return pattern.matcher(line.substring(i)).lookingAt(); } /** diff --git a/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java b/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java index 45e7efd..15f2ad0 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java @@ -970,6 +970,40 @@ class CompletionResolverTest { assertTrue(notified.get(0).contains("503 Service Unavailable"), notified.get(0)); } + /** + * fleetd #339 follow-up: a genuine backend error rendered behind terminal chrome must still + * record the credential outage. #339 added a start-of-line check to stop a member's own prose + * being counted as an outage, and a bare {@code lookingAt} also rejected this — the send failed + * but the sink never fired. #339's invariant 3 named that direction as the worse one: a real + * outage going unrecorded leaves the fleet spawning into a dead credential. + * + *
The raw-scrape path is where this matters, because its own comment says to expect leading
+ * TUI chrome there.
+ */
+ @Test
+ void aRealErrorBehindTerminalChromeStillNotifiesTheSink() {
+ String block = """
+ ╭──────────────────────────────────────╮
+ │ 503 Service Unavailable: upstream credential rejected
+ """;
+ FakeHerdr herdr = new FakeHerdr().readText(block);
+ Rendezvous rendezvous = new Rendezvous();
+ BackendErrorPatternLookup patterns = target -> Pattern.compile("(?i)503 Service Unavailable");
+ java.util.List