fleetd #775: pane-placement guard trigger keys on lead existence, not tab:
The guard's lead half now fires whenever fleet.leaders has any entry, since a lead's tab is always labelled by the fixed LEAD_TAB_LABEL constant regardless of its own deprecated tab: field. The refusal message no longer advises removing a lead's tab:, which cannot satisfy the guard any more.
This commit is contained in:
@@ -2956,30 +2956,32 @@ public record FleetConfig(
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a profile that places its members by {@code "pane"} while any {@code fleet.leaders}
|
||||
* or {@code fleet.collaborators} entry names a {@code tab}. A pane-placed member lands inside
|
||||
* the focused tab rather than its own, so it can land inside a lead's or collaborator's own
|
||||
* labelled tab. {@link dev.ltms.fleet.herdr.LeadTabScanner} identifies a lead or collaborator
|
||||
* purely by that tab's label — it does not exclude the member space — so a member that ends up
|
||||
* there, while its pane carries no entry in the spawned-member roster, is read back as that
|
||||
* lead or collaborator and granted that identity's authority.
|
||||
* Reject a profile that places its members by {@code "pane"} while {@code fleet.leaders} has
|
||||
* any entry, or any {@code fleet.collaborators} entry names a {@code tab}. A pane-placed
|
||||
* member lands inside the focused tab rather than its own, so it can land inside a lead's or
|
||||
* collaborator's own labelled tab. {@link dev.ltms.fleet.herdr.LeadTabScanner} identifies a
|
||||
* lead or collaborator purely by that tab's label — it does not exclude the member space — so
|
||||
* a member that ends up there, while its pane carries no entry in the spawned-member roster,
|
||||
* is read back as that lead or collaborator and granted that identity's authority.
|
||||
*
|
||||
* <p>Only an entry with a non-blank {@code tab} is in scope: one with no {@code tab} feeds
|
||||
* <p>Every {@code fleet.leaders} entry is in scope regardless of its own {@code tab} field:
|
||||
* {@link Leader#acceptedLabels()} always includes {@link Leader#LEAD_TAB_LABEL}. Only a
|
||||
* collaborator with a non-blank {@code tab} is in scope: one with no {@code tab} feeds
|
||||
* nothing into {@link dev.ltms.fleet.herdr.LeadTabScanner}, so it creates no hazard here.
|
||||
*
|
||||
* @throws IllegalStateException when any {@code profiles:} entry is pane-placed while any
|
||||
* {@code fleet.leaders} or {@code fleet.collaborators} entry
|
||||
* names a non-blank {@code tab}
|
||||
* @throws IllegalStateException when any {@code profiles:} entry is pane-placed while
|
||||
* {@code fleet.leaders} is non-empty, or any
|
||||
* {@code fleet.collaborators} entry names a non-blank
|
||||
* {@code tab}
|
||||
*/
|
||||
public void validatePanePlacementAgainstLeadTabs() {
|
||||
if (fleet == null) {
|
||||
return;
|
||||
}
|
||||
boolean anyLeaderHasTab = fleet.leaders().values().stream()
|
||||
.anyMatch(leader -> leader != null && leader.tab() != null && !leader.tab().isBlank());
|
||||
boolean anyLeaderExists = !fleet.leaders().isEmpty();
|
||||
boolean anyCollaboratorHasTab = fleet.collaborators().values().stream()
|
||||
.anyMatch(c -> c != null && c.tab() != null && !c.tab().isBlank());
|
||||
if (!anyLeaderHasTab && !anyCollaboratorHasTab) {
|
||||
if (!anyLeaderExists && !anyCollaboratorHasTab) {
|
||||
return;
|
||||
}
|
||||
List<String> bad = new ArrayList<>();
|
||||
@@ -2996,8 +2998,8 @@ public record FleetConfig(
|
||||
+ "pane-placed member can land inside that labelled tab, and while its pane "
|
||||
+ "carries no entry in the spawned-member roster, it is read back as the lead or "
|
||||
+ "collaborator and granted that identity's authority. Set placement: tab for "
|
||||
+ "each named profile, or remove the tab from every fleet.leaders and "
|
||||
+ "fleet.collaborators entry.");
|
||||
+ "each named profile, or remove the fleet.leaders entry, or remove the tab from "
|
||||
+ "each fleet.collaborators entry.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -923,8 +923,12 @@ class FleetConfigTest {
|
||||
assertTrue(e.getMessage().contains("gx10"), "the message must name the offending profile");
|
||||
}
|
||||
|
||||
/**
|
||||
* A lead is found by its fixed tab label regardless of its own {@code tab} field, so a
|
||||
* {@code fleet.leaders} entry with no {@code tab} must still arm the guard.
|
||||
*/
|
||||
@Test
|
||||
void aPanePlacedProfileWithNoLeadTabIsAllowed(@TempDir Path dir) throws Exception {
|
||||
void aPanePlacedProfileWithNoLeadTabRefusesToStart(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("pane-no-tab.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
@@ -937,9 +941,59 @@ class FleetConfigTest {
|
||||
opus:
|
||||
profile: gx10
|
||||
""");
|
||||
FleetConfig cfg = FleetConfig.load(f);
|
||||
|
||||
IllegalStateException e = assertThrows(IllegalStateException.class,
|
||||
cfg::validatePanePlacementAgainstLeadTabs);
|
||||
assertTrue(e.getMessage().contains("gx10"), "the message must name the offending profile");
|
||||
assertTrue(e.getMessage().contains("remove the fleet.leaders entry"),
|
||||
"the message must offer removing the leaders entry, since removing tab: no longer works");
|
||||
assertFalse(e.getMessage().contains("remove the tab from every fleet.leaders"),
|
||||
"the message must not send the operator in a circle by advising a tab: removal");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code placement:} is optional, and {@link FleetConfig.Profile}'s own compact constructor
|
||||
* defaults an absent or blank value to {@code "tab"}, so a profile naming no placement at all
|
||||
* is tab-placed and the guard must not fire for it.
|
||||
*/
|
||||
@Test
|
||||
void aProfileWithNoPlacementKeyDefaultsToTabPlacementAndIsAllowed(@TempDir Path dir)
|
||||
throws Exception {
|
||||
Path f = dir.resolve("no-placement-key.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
port: 8080
|
||||
profiles:
|
||||
gx10: {}
|
||||
fleet:
|
||||
leaders:
|
||||
opus:
|
||||
profile: gx10
|
||||
""");
|
||||
FleetConfig cfg = FleetConfig.load(f);
|
||||
|
||||
assertTrue(cfg.profiles().get("gx10").tabPlacement(),
|
||||
"Profile's compact constructor defaults an absent placement to \"tab\"");
|
||||
assertDoesNotThrow(cfg::validatePanePlacementAgainstLeadTabs,
|
||||
"a profile with no placement: key is tab-placed, not pane-placed");
|
||||
}
|
||||
|
||||
/** Control: no {@code fleet.leaders} entry and no collaborator tab still starts fine. */
|
||||
@Test
|
||||
void aPanePlacedProfileWithAnEmptyFleetBlockIsAllowed(@TempDir Path dir) throws Exception {
|
||||
Path f = dir.resolve("pane-empty-fleet.yaml");
|
||||
Files.writeString(f, """
|
||||
bind:
|
||||
port: 8080
|
||||
profiles:
|
||||
gx10:
|
||||
placement: pane
|
||||
fleet: {}
|
||||
""");
|
||||
|
||||
assertDoesNotThrow(() -> FleetConfig.load(f).validatePanePlacementAgainstLeadTabs(),
|
||||
"a leader with no tab feeds nothing into the scanner, so pane placement is safe");
|
||||
"no fleet.leaders entry and no collaborator tab means pane placement is safe");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user