diff --git a/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java b/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java index eddc419..30a7081 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java +++ b/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java @@ -380,8 +380,8 @@ public final class Fleetd { .map(session -> exhaustedPatternsByProfile.get(session.profile())) .orElse(null); log.info("backend-exhausted classification (CB-578 stage A): {}", - CompletionResolver.coverage("exhaustedPattern", cfg.profiles().keySet(), - exhaustedPatternsByProfile.keySet())); + CompletionResolver.coverage("exhaustedPattern", CompletionResolver.UnsetMeaning.OFF, + cfg.profiles().keySet(), exhaustedPatternsByProfile.keySet())); // fleetd #201 Unit 5: classify a completion-fallback scrape that matches a profile's // configured backend-error refusal (a credential outage, a provider 5xx) as a backend error // rather than handing it back as a real answer. Compiled once at startup, keyed by profile @@ -401,8 +401,8 @@ public final class Fleetd { BackendErrorPatternLookup backendErrorPatterns = backendErrorPatternLookup(sessions::roster, errorPatternsByProfile); log.info("backend-error classification (fleetd #201 Unit 5): {}", - CompletionResolver.coverage("errorPattern", cfg.profiles().keySet(), - errorPatternsByProfile.keySet())); + CompletionResolver.coverage("errorPattern", CompletionResolver.UnsetMeaning.BUILT_IN_DEFAULT, + cfg.profiles().keySet(), errorPatternsByProfile.keySet())); // CB-578 stage B: on a classification that actually wins, quarantine the exhausted profile's // CREDENTIAL — not the profile name — so a profile sharing that credential (e.g. two models // on one OpenAI account) is refused too, not just the one that happened to report it. Reads 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 7ad8406..697569d 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java +++ b/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java @@ -698,17 +698,47 @@ public final class CompletionResolver implements TurnListener { } /** - * Coverage summary for the CB-578 stage A exhausted-pattern classification, logged at startup + * What an unset pattern key means for the classification it configures (fleetd#415). + * {@code coverage()} cannot infer this from the key's name — the two keys it currently + * describes disagree on it, and a string comparison on the name would just move the same bug + * to a new spot — so every caller must state it explicitly. + */ + public enum UnsetMeaning { + /** No fallback exists: a profile with no configured pattern truly has this classification off. */ + OFF, + /** A built-in pattern applies when unset: the classification still runs for that profile. */ + BUILT_IN_DEFAULT + } + + /** + * Coverage summary for a fleetd#201/CB-578-style pattern-key classification, logged at startup * the way {@link dev.ltms.fleet.health.FleetHealthMonitor#coverage} is — so an operator can * see whether the classification is on, and for which profiles, without reading every * profile's config by hand. * + *
fleetd#415: this method measures pattern coverage — how many profiles set the
+ * key — which is not the same thing as feature state for a key with a fallback. For
+ * {@code errorPattern}, an empty {@code configuredProfiles} still runs the classification
+ * against {@code CompletionResolver}'s built-in compatibility pattern ({@link #BACKEND_ERROR}
+ * at line ~84); for {@code exhaustedPattern} there is no fallback, so empty really does mean
+ * off. {@code unsetMeaning} is the single, required source of that fact — see
+ * {@link dev.ltms.fleet.config.FleetConfig#rejectMalformedProfilePatterns} lines ~2029-2032 for
+ * where it is documented for config authors. It is a required parameter, not a defaulted
+ * overload: a third pattern key added later must supply one to compile at all, rather than
+ * silently inheriting whichever wording this method happened to default to.
+ *
* @param allProfiles every configured profile name
- * @param configuredProfiles the subset of {@code allProfiles} that carry an exhausted pattern
+ * @param configuredProfiles the subset of {@code allProfiles} that carry the pattern
*/
- public static String coverage(String patternKey, Set Every earlier test here passed the exhaustion case only, so none of them could see it. This
* one pins that the message names the key the caller actually meant.
+ *
+ * fleetd#415: the expected wording changed here too. {@code errorPattern} has a built-in
+ * fallback ({@link CompletionResolver#BACKEND_ERROR}), so an empty {@code configuredProfiles}
+ * for it is not "off" — see {@link #coverageDistinguishesOffFromBuiltInDefaultForTheSameEmptyInput}
+ * for the test built specifically to pin that distinction.
*/
@Test
void coverageNamesTheConfigKeyItsCallerMeansRatherThanAlwaysSayingExhaustedPattern() {
- assertEquals("off (no profile has an errorPattern configured; profiles: [gx10, terra])",
- CompletionResolver.coverage("errorPattern", Set.of("terra", "gx10"), Set.of()));
+ assertEquals("built-in default for all profiles (no profile customises errorPattern; "
+ + "profiles: [gx10, terra])",
+ CompletionResolver.coverage("errorPattern", CompletionResolver.UnsetMeaning.BUILT_IN_DEFAULT,
+ Set.of("terra", "gx10"), Set.of()));
+ }
+
+ /**
+ * fleetd#415: {@code coverage()} measures pattern coverage (how many profiles set the key), but
+ * for {@code errorPattern} the empty case is not the feature-off state — a profile with no
+ * configured {@code errorPattern} still runs the classification against
+ * {@link CompletionResolver#BACKEND_ERROR}. For {@code exhaustedPattern} there is no fallback,
+ * so empty really is off. Same shape of input (empty {@code configuredProfiles}, one profile),
+ * different {@link CompletionResolver.UnsetMeaning} — the wording must differ, or this method is
+ * back to conflating pattern coverage with feature state for the one key where they disagree.
+ */
+ @Test
+ void coverageDistinguishesOffFromBuiltInDefaultForTheSameEmptyInput() {
+ String exhaustedLine = CompletionResolver.coverage("exhaustedPattern",
+ CompletionResolver.UnsetMeaning.OFF, Set.of("gx10", "terra"), Set.of());
+ String errorLine = CompletionResolver.coverage("errorPattern",
+ CompletionResolver.UnsetMeaning.BUILT_IN_DEFAULT, Set.of("gx10", "terra"), Set.of());
+
+ assertEquals("off (no profile has an exhaustedPattern configured; profiles: [gx10, terra])",
+ exhaustedLine);
+ assertEquals("built-in default for all profiles (no profile customises errorPattern; "
+ + "profiles: [gx10, terra])", errorLine);
+ assertNotEquals(exhaustedLine, errorLine,
+ "the same empty-coverage input must not read as the same feature state for both keys");
}
// --- fleetd#201 Unit 1: target-keyed backend-error pattern + typed sink ----------------------