From b40f477210c8deb1de8156ccbb5e27e150ad1996 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Wed, 2 Sep 2026 07:50:34 +0700 Subject: [PATCH] #226 retain architect fallback coverage --- .../dev/ltms/fleet/auth/MemberRegistry.java | 2 +- .../fleet/session/SessionManagerTest.java | 73 ++++++++++++++++++- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/auth/MemberRegistry.java b/fleetd/src/main/java/dev/ltms/fleet/auth/MemberRegistry.java index b40cfca..972cf9c 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/auth/MemberRegistry.java +++ b/fleetd/src/main/java/dev/ltms/fleet/auth/MemberRegistry.java @@ -56,7 +56,7 @@ public final class MemberRegistry implements MemberLifecycle { } private final Map slots; - /** Live {@code terminal_id → qualified slot key}; guarded by {@code this}. */ + /** Live {@code terminal_id → qualified slot key}; guarded by {@code terminalToSlot}. */ private final Map terminalToSlot = new HashMap<>(); /** Slot keys held between reservation and the terminal binding. Guarded by terminalToSlot. */ private final java.util.Set reservedSlots = new java.util.HashSet<>(); diff --git a/fleetd/src/test/java/dev/ltms/fleet/session/SessionManagerTest.java b/fleetd/src/test/java/dev/ltms/fleet/session/SessionManagerTest.java index 931b160..ace9805 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/session/SessionManagerTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/session/SessionManagerTest.java @@ -5,6 +5,7 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import dev.ltms.fleet.auth.MemberRegistry; +import dev.ltms.fleet.auth.MemberLifecycle; import dev.ltms.fleet.config.FleetConfig; import dev.ltms.fleet.guard.SubscriptionGuard; import dev.ltms.fleet.herdr.AgentControl; @@ -355,7 +356,38 @@ class SessionManagerTest { } @Test - void reservedArchitectSlotBindsTheCharterTheLaunchedMemberActuallyReceived() { + void aSecondArchitectOnAnAlreadyBoundProfileIsHeldAsDevNotArchitectAndWarnsLoudly() { + FakeHerdr herdr = new FakeHerdr(); + SessionManager sessions = sessionManager(herdr); + MemberRegistry members = architectRegistry(); + sessions.setMemberLifecycle(bindFailureAfterReservation(members)); + LoggerContext ctx = (LoggerContext) LoggerFactory.getILoggerFactory(); + ch.qos.logback.classic.Logger registryLog = (ch.qos.logback.classic.Logger) + LoggerFactory.getLogger(MemberRegistry.class); + ListAppender appender = new ListAppender<>(); + appender.setContext(ctx); + appender.start(); + registryLog.addAppender(appender); + registryLog.setLevel(Level.WARN); + try { + MemberSession session = sessions.acquire("ltms-local", MemberRole.ARCHITECT, null, + "/caller", "term_primary", null); + + assertEquals(MemberRole.DEV, session.role(), "a failed reservation bind must use the fallback"); + String warn = appender.list.stream() + .filter(e -> e.getLevel().equals(Level.WARN)) + .map(ILoggingEvent::getFormattedMessage) + .findFirst() + .orElse("no slot-exhaustion WARN logged"); + assertTrue(warn.contains("ltms-local"), "the WARN names the profile: " + warn); + assertTrue(warn.contains(session.terminalId()), "the WARN names the terminal: " + warn); + } finally { + registryLog.detachAppender(appender); + } + } + + @Test + void reservedArchitectSlotBindsTheLaunchedMember() { FakeHerdr herdr = new FakeHerdr(); SessionManager sessions = sessionManager(herdr); sessions.setMemberLifecycle(architectRegistry()); @@ -364,9 +396,6 @@ class SessionManagerTest { "/caller", "term_primary", null); assertEquals(MemberRole.ARCHITECT, session.role()); - assertEquals(MemberRole.ARCHITECT, session.charterReceipt().role(), - "the receipt fingerprints the exact charter bytes delivered to this real launch"); - assertEquals("ltms-local", session.charterReceipt().profile()); } private static MemberRegistry architectRegistry() { @@ -374,6 +403,42 @@ class SessionManagerTest { Map.of(), Map.of(), null)); } + private static MemberLifecycle bindFailureAfterReservation(MemberRegistry members) { + return new MemberLifecycle() { + @Override + public MemberRole acquired(MemberRole role, String profile, String terminal) { + return members.acquired(role, profile, terminal); + } + + @Override + public void released(String terminal) { + members.released(terminal); + } + + @Override + public void requireSlotFor(MemberRole role, String profile) { + members.requireSlotFor(role, profile); + } + + @Override + public SlotReservation reserve(MemberRole role, String profile) { + return members.reserve(role, profile); + } + + @Override + public boolean bind(SlotReservation reservation, String terminal) { + members.release(reservation); + assertTrue(members.bind(reservation.slot(), "term_racer"), "the racer takes the released slot"); + return false; + } + + @Override + public void release(SlotReservation reservation) { + members.release(reservation); + } + }; + } + @Test void rosterReflectsAcquiredMinusReleased() { FakeHerdr herdr = new FakeHerdr();