fleetd #431: pin profileForSlot, isSlot and nameForSlot against a live reload
#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.
This commit is contained in:
@@ -69,6 +69,22 @@ class MemberRegistryLiveTest {
|
|||||||
profile: sonnet
|
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) {
|
private static ConfigRef refFor(Path f) {
|
||||||
return new ConfigRef(f, FleetConfig.load(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");
|
"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) ───────
|
// ── 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
|
// 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
|
// very next request, but the terminalToSlot BINDING itself is untouched by a reload — dropping
|
||||||
|
|||||||
Reference in New Issue
Block a user