From 44c735f6f5f73d199678bc5370e539c45b73e21a Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Thu, 10 Sep 2026 08:56:39 +0700 Subject: [PATCH] fleetd #404: report armed detection from startup map --- .../src/main/java/dev/ltms/fleet/Fleetd.java | 8 +- ...etdExhaustionDetectionArmedWiringTest.java | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 fleetd/src/test/java/dev/ltms/fleet/FleetdExhaustionDetectionArmedWiringTest.java diff --git a/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java b/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java index cc9191d..3372f49 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java +++ b/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java @@ -669,11 +669,9 @@ public final class Fleetd { var configured = config.get().profiles().get(profile); return configured == null ? null : configured.effectiveCredentialId(); }, quarantine, profile -> { - // fleetd #395: read live off the current config, like credentialIdFor above — an - // exhaustedPattern edit takes effect on the next fleet_profiles/fleet_list call, no - // restart needed, same as the credential-id lookup it sits beside. - var configured = config.get().profiles().get(profile); - return configured != null && configured.hasExhaustedPattern(); + // fleetd #404: use the compiled startup map that CompletionResolver reads. An + // exhaustedPattern edit is deferred, so this stays aligned with detection until restart. + return exhaustedPatternsByProfile.containsKey(profile); }); FleetMcp.OutageSource outageSource = new FleetMcp.OutageSource(profile -> { var configured = config.get().profiles().get(profile); diff --git a/fleetd/src/test/java/dev/ltms/fleet/FleetdExhaustionDetectionArmedWiringTest.java b/fleetd/src/test/java/dev/ltms/fleet/FleetdExhaustionDetectionArmedWiringTest.java new file mode 100644 index 0000000..0d28635 --- /dev/null +++ b/fleetd/src/test/java/dev/ltms/fleet/FleetdExhaustionDetectionArmedWiringTest.java @@ -0,0 +1,87 @@ +package dev.ltms.fleet; + +import dev.ltms.fleet.config.ConfigRef; +import dev.ltms.fleet.config.FleetConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * fleetd #404: {@code exhaustionDetectionArmed} must describe the startup pattern map that + * {@code CompletionResolver} actually uses, not the reloaded config snapshot. + * + *

This test needs a reload. At startup the two snapshots agree, so a test of only a newly + * started daemon would not detect a live {@code config.get()} lookup in the report field. + */ +class FleetdExhaustionDetectionArmedWiringTest { + + private static final String NO_PATTERN = """ + bind: + host: 127.0.0.1 + port: 8765 + herdrSocket: ~/.config/herdr/herdr.sock + profiles: + terra: + baseUrl: http://gx00.gw:8000 + model: terra + guard: + offSubscriptionHosts: + - gx00.gw + """; + + private static final String WITH_PATTERN = """ + bind: + host: 127.0.0.1 + port: 8765 + herdrSocket: ~/.config/herdr/herdr.sock + profiles: + terra: + baseUrl: http://gx00.gw:8000 + model: terra + exhaustedPattern: "usage limit" + guard: + offSubscriptionHosts: + - gx00.gw + """; + + private static Map compiledPatterns(FleetConfig config) { + Map patterns = new LinkedHashMap<>(); + config.profiles().forEach((name, profile) -> { + if (profile.hasExhaustedPattern()) { + patterns.put(name, Pattern.compile(profile.exhaustedPattern())); + } + }); + return patterns; + } + + private static String fleetdSource() throws Exception { + return Files.readString(Path.of("src/main/java/dev/ltms/fleet/Fleetd.java")); + } + + @Test + @DisplayName("[SOURCE TEXT] reloading an exhaustedPattern cannot make the armed field read config.get()") + void reloadedPatternDoesNotChangeTheArmedFieldUntilRestart(@TempDir Path dir) throws Exception { + Path file = dir.resolve("fleetd.yaml"); + Files.writeString(file, NO_PATTERN); + ConfigRef config = new ConfigRef(file, FleetConfig.load(file)); + Map startupPatterns = compiledPatterns(config.get()); + + Files.writeString(file, WITH_PATTERN); + assertTrue(config.reload().applied()); + assertTrue(config.get().profiles().get("terra").hasExhaustedPattern()); + assertFalse(startupPatterns.containsKey("terra")); + + assertTrue(fleetdSource().contains("return exhaustedPatternsByProfile.containsKey(profile);"), + "Fleetd must derive exhaustionDetectionArmed from exhaustedPatternsByProfile, not config.get(): " + + "the compiled startup map is what CompletionResolver uses until restart."); + } +}