From a196d34455e4364c6a4109b2b819771ae9beb27f Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Thu, 10 Sep 2026 13:06:56 +0700 Subject: [PATCH] fleetd #431: pin profileForSlot, isSlot and nameForSlot against a live reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #424 made MemberRegistry.slots() re-read fleet: on every call, but only roleForSlot was tested against a real reload. profileForSlot, isSlot and nameForSlot all have the same live-read line and none was pinned — proved by freezing each to a construction-time snapshot and watching the full suite stay green. Adds 4 tests to MemberRegistryLiveTest, each driving a real ConfigRef.reload() against a @TempDir config file (never two frozen registries compared in memory, which would test the constructor instead of the reload): - profileForSlotReflectsAProfileChangedByReload - isSlotStopsReportingASlotRemovedByReload / isSlotStartsReportingASlotAddedByReload - nameForSlotReflectsANameChangedByReload No production change. profileForSlot has no call site anywhere in src/main yet, so there is no spawn-lifecycle seam to drive the test through beyond the accessor itself. --- .../fleet/auth/MemberRegistryLiveTest.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/fleetd/src/test/java/dev/ltms/fleet/auth/MemberRegistryLiveTest.java b/fleetd/src/test/java/dev/ltms/fleet/auth/MemberRegistryLiveTest.java index ea177e1..fde51aa 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/auth/MemberRegistryLiveTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/auth/MemberRegistryLiveTest.java @@ -69,6 +69,22 @@ class MemberRegistryLiveTest { profile: sonnet """; + /** Same slot name ({@code designer}) as {@link #WITH_SONNET_SLOT}, repointed to a different profile. */ + private static final String WITH_OPUS_SLOT = """ + fleet: + architects: + designer: + profile: opus + """; + + /** Same profile ({@code sonnet}) as {@link #WITH_SONNET_SLOT}, but the pool key is renamed. */ + private static final String WITH_RENAMED_SLOT = """ + fleet: + architects: + architect-lead: + profile: sonnet + """; + private static ConfigRef refFor(Path f) { return new ConfigRef(f, FleetConfig.load(f)); } @@ -155,6 +171,88 @@ class MemberRegistryLiveTest { "a slot added by reload must be reservable with no restart"); } + // ── profileForSlot, isSlot and nameForSlot are live too (fleetd #431) ───────────────────────── + // #424 pinned roleForSlot against a live reload but left these three untested — proved by + // mutating each to read a snapshot flattened once at construction: the full suite stayed green + // for all three. profileForSlot is the one with a real production stake: the spawn lifecycle + // reads it to pick an architect slot's backend, and fleetd has no call site for it yet (grepped + // "profileForSlot" across src/main — only this file and MemberRegistryTest reference it), so + // there is no seam to drive this test through beyond the accessor itself. + + @Test + void profileForSlotReflectsAProfileChangedByReload(@TempDir Path dir) throws Exception { + Path f = dir.resolve("fleetd.yaml"); + Files.writeString(f, yaml(WITH_SONNET_SLOT)); + ConfigRef ref = refFor(f); + MemberRegistry registry = MemberRegistry.live(() -> ref.get().fleet()); + + assertEquals("sonnet", registry.profileForSlot("architect:designer"), + "the slot's profile before the reload"); + + Files.writeString(f, yaml(WITH_OPUS_SLOT)); + ConfigRef.Outcome out = ref.reload(); + assertTrue(out.applied(), "the reload must actually take effect: " + out.summary()); + + assertEquals("opus", registry.profileForSlot("architect:designer"), + "repointing the slot to a different profile must take effect with no restart — " + + "this is what the spawn lifecycle reads to pick an architect's backend"); + } + + @Test + void isSlotStopsReportingASlotRemovedByReload(@TempDir Path dir) throws Exception { + Path f = dir.resolve("fleetd.yaml"); + Files.writeString(f, yaml(WITH_SONNET_SLOT)); + ConfigRef ref = refFor(f); + MemberRegistry registry = MemberRegistry.live(() -> ref.get().fleet()); + + assertTrue(registry.isSlot("architect:designer"), "the slot is configured before the reload"); + + Files.writeString(f, yaml(WITHOUT_ARCHITECT_SLOTS)); + ConfigRef.Outcome out = ref.reload(); + assertTrue(out.applied(), "the reload must actually take effect: " + out.summary()); + + assertFalse(registry.isSlot("architect:designer"), + "removing the slot from config must make isSlot say so on the very next call, " + + "with no restart"); + } + + @Test + void isSlotStartsReportingASlotAddedByReload(@TempDir Path dir) throws Exception { + Path f = dir.resolve("fleetd.yaml"); + Files.writeString(f, yaml(WITHOUT_ARCHITECT_SLOTS)); + ConfigRef ref = refFor(f); + MemberRegistry registry = MemberRegistry.live(() -> ref.get().fleet()); + + assertFalse(registry.isSlot("architect:designer"), "no architect slot is configured yet"); + + Files.writeString(f, yaml(WITH_SONNET_SLOT)); + ConfigRef.Outcome out = ref.reload(); + assertTrue(out.applied(), "the reload must actually take effect: " + out.summary()); + + assertTrue(registry.isSlot("architect:designer"), + "a slot added by reload must be visible to isSlot with no restart"); + } + + @Test + void nameForSlotReflectsANameChangedByReload(@TempDir Path dir) throws Exception { + Path f = dir.resolve("fleetd.yaml"); + Files.writeString(f, yaml(WITH_SONNET_SLOT)); + ConfigRef ref = refFor(f); + MemberRegistry registry = MemberRegistry.live(() -> ref.get().fleet()); + + assertEquals("designer", registry.nameForSlot("architect:designer"), + "the configured name before the reload"); + + Files.writeString(f, yaml(WITH_RENAMED_SLOT)); + ConfigRef.Outcome out = ref.reload(); + assertTrue(out.applied(), "the reload must actually take effect: " + out.summary()); + + assertNull(registry.nameForSlot("architect:designer"), + "the old key no longer names a configured slot — it was renamed away by the reload"); + assertEquals("architect-lead", registry.nameForSlot("architect:architect-lead"), + "the new name must be visible under its new qualified key with no restart"); + } + // ── a bound architect is demoted, but the binding itself is not touched (fleetd #424) ─────── // The lead's corrected ruling: the PRIVILEGE a slot grants is revoked on the bound session's // very next request, but the terminalToSlot BINDING itself is untouched by a reload — dropping -- 2.52.0