From 6c1c2c39945e29b3d790b4731a54f67cd9efa954 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sat, 15 Aug 2026 05:45:39 +0200 Subject: [PATCH] CB-573: report empty configured profile capacity --- .../main/java/dev/ltms/bridged/Bridged.java | 2 +- .../java/dev/ltms/bridged/mcp/BridgeMcp.java | 20 ++++++++++++------- .../dev/ltms/bridged/mcp/BridgeMcpTest.java | 14 ++++++++++++- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/bridged/src/main/java/dev/ltms/bridged/Bridged.java b/bridged/src/main/java/dev/ltms/bridged/Bridged.java index 8349fcb..f0de87c 100644 --- a/bridged/src/main/java/dev/ltms/bridged/Bridged.java +++ b/bridged/src/main/java/dev/ltms/bridged/Bridged.java @@ -387,7 +387,7 @@ public final class Bridged { profile -> { var configured = config.get().profiles().get(profile); return configured == null ? null : configured.maxLoad(); - }, System::nanoTime); + }, System::nanoTime, () -> config.get().profiles().keySet()); // CB-559: opt-in config reload. With no `configReload:` block nothing is constructed, so an // upgraded daemon behaves exactly as before — the file is read once at boot and never again. diff --git a/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java b/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java index 6c5a960..7c5426a 100644 --- a/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java +++ b/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java @@ -36,6 +36,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.LongSupplier; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -78,6 +79,7 @@ public final class BridgeMcp { private final Function liveCount; private final Function maxLoad; private final LongSupplier clock; + private final Supplier> configuredProfiles; /** * Legacy constructor — no authorization. Retained so existing tests exercise tool behaviour @@ -86,7 +88,7 @@ public final class BridgeMcp { public BridgeMcp(MessageService messages, PeerLauncher workers, SessionManager sessions, ConnectionIdentity identity, MemberPresence presence, PrimaryRegistry primaryRegistry) { - this(messages, workers, sessions, identity, presence, primaryRegistry, null, null, _ -> 0, _ -> null, System::nanoTime); + this(messages, workers, sessions, identity, presence, primaryRegistry, null, null, _ -> 0, _ -> null, System::nanoTime, Set::of); } /** @@ -100,16 +102,17 @@ public final class BridgeMcp { SessionManager sessions, ConnectionIdentity identity, MemberPresence presence, PrimaryRegistry primaryRegistry, CallerResolver callers, Metrics metrics) { this(messages, workers, sessions, identity, presence, primaryRegistry, callers, metrics, - _ -> 0, _ -> null, System::nanoTime); + _ -> 0, _ -> null, System::nanoTime, Set::of); } public BridgeMcp(MessageService messages, PeerLauncher workers, SessionManager sessions, ConnectionIdentity identity, MemberPresence presence, PrimaryRegistry primaryRegistry, CallerResolver callers, Metrics metrics, Function liveCount, - Function maxLoad, LongSupplier clock) { + Function maxLoad, LongSupplier clock, Supplier> configuredProfiles) { this.liveCount = liveCount; this.maxLoad = maxLoad; this.clock = clock; + this.configuredProfiles = configuredProfiles; McpJsonMapper json = new JacksonMcpJsonMapperSupplier().get(); this.transport = HttpServletStreamableServerTransportProvider.builder() .jsonMapper(json) @@ -223,7 +226,7 @@ public final class BridgeMcp { .toolCall(listTool(), (exchange, _) -> { McpSchema.CallToolResult denied = deny(exchange, Authz.Action.READ, null); if (denied != null) return denied; - return listFleet(workers, sessions, messages, liveCount, maxLoad, clock, + return listFleet(workers, sessions, messages, liveCount, maxLoad, clock, configuredProfiles.get(), callers == null ? Map.of() : callers.leads(), callerTerminal(exchange)); }) @@ -734,12 +737,13 @@ public final class BridgeMcp { */ static McpSchema.CallToolResult listFleet(PeerLauncher workers, SessionManager sessions, Map leads, String selfTerm) { - return listFleet(workers, sessions, null, _ -> 0, _ -> null, System::nanoTime, leads, selfTerm); + return listFleet(workers, sessions, null, _ -> 0, _ -> null, System::nanoTime, Set.of(), leads, selfTerm); } static McpSchema.CallToolResult listFleet(PeerLauncher workers, SessionManager sessions, MessageService messages, Function liveCount, Function maxLoad, - LongSupplier clock, Map leads, String selfTerm) { + LongSupplier clock, Set configuredProfiles, + Map leads, String selfTerm) { try { Map live = workers.list().stream() .map(Agent.class::cast) @@ -753,7 +757,9 @@ public final class BridgeMcp { List> out = roster.stream() .map(s -> memberCapacityView(s, live.get(s.terminalId()), messages, clock.getAsLong())) .toList(); - List> capacity = roster.stream().map(MemberSession::profile).distinct().sorted() + Set profiles = new java.util.TreeSet<>(configuredProfiles); + roster.stream().map(MemberSession::profile).forEach(profiles::add); + List> capacity = profiles.stream() .map(profile -> capacityView(profile, liveCount, maxLoad, roster, messages, clock.getAsLong())) .toList(); return text(json(Map.of("leads", leadRows, "members", out, "capacity", capacity))); diff --git a/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpTest.java b/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpTest.java index 7110233..fef949f 100644 --- a/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpTest.java @@ -348,13 +348,25 @@ class BridgeMcpTest { SessionManager sessions = new SessionManager(workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw"))); sessions.acquire("ltms-local", null, null, null); McpSchema.CallToolResult res = BridgeMcp.listFleet(workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw")), - sessions, null, profile -> 2, profile -> 2, () -> 0, Map.of(), ""); + sessions, null, profile -> 2, profile -> 2, () -> 0, Set.of("ltms-local"), Map.of(), ""); String out = textOf(res); assertTrue(out.contains("\"maxLoad\":2"), out); assertTrue(out.contains("\"live\":2"), out); assertTrue(out.contains("\"free\":0"), out); } + @Test + void capacityIncludesConfiguredProfileWithoutMembers() { + FakeHerdr h = new FakeHerdr(); + SessionManager sessions = new SessionManager(workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw"))); + String out = textOf(BridgeMcp.listFleet(workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw")), + sessions, null, profile -> 0, profile -> 2, () -> 0, Set.of("terra"), Map.of(), "")); + assertTrue(out.contains("\"profile\":\"terra\""), out); + assertTrue(out.contains("\"live\":0"), out); + assertTrue(out.contains("\"free\":2"), out); + assertTrue(out.contains("\"reclaimable\":0"), out); + } + @Test void listReportsLeadsAndFlagsTheCallersOwnRow() { FakeHerdr h = new FakeHerdr();