CB-573: report empty configured profile capacity
CI / build (pull_request) Successful in 59s
CI / contract (pull_request) Successful in 1m0s

This commit is contained in:
Dai Ha
2026-08-15 05:45:39 +02:00
parent 01fab15713
commit 6c1c2c3994
3 changed files with 27 additions and 9 deletions
@@ -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.
@@ -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<String, Integer> liveCount;
private final Function<String, Integer> maxLoad;
private final LongSupplier clock;
private final Supplier<Set<String>> 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<String, Integer> liveCount,
Function<String, Integer> maxLoad, LongSupplier clock) {
Function<String, Integer> maxLoad, LongSupplier clock, Supplier<Set<String>> 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<String, String> 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<String, Integer> liveCount, Function<String, Integer> maxLoad,
LongSupplier clock, Map<String, String> leads, String selfTerm) {
LongSupplier clock, Set<String> configuredProfiles,
Map<String, String> leads, String selfTerm) {
try {
Map<String, Agent> live = workers.list().stream()
.map(Agent.class::cast)
@@ -753,7 +757,9 @@ public final class BridgeMcp {
List<Map<String, Object>> out = roster.stream()
.map(s -> memberCapacityView(s, live.get(s.terminalId()), messages, clock.getAsLong()))
.toList();
List<Map<String, Object>> capacity = roster.stream().map(MemberSession::profile).distinct().sorted()
Set<String> profiles = new java.util.TreeSet<>(configuredProfiles);
roster.stream().map(MemberSession::profile).forEach(profiles::add);
List<Map<String, Object>> 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)));
@@ -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();