From 043b3290c487d8bf39074887d072b55d6f0e17e2 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Mon, 5 Oct 2026 14:09:19 +0200 Subject: [PATCH 1/2] 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. --- .../dev/ltms/fleet/config/FleetConfig.java | 34 ++++++----- .../ltms/fleet/config/FleetConfigTest.java | 58 ++++++++++++++++++- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java b/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java index 4e0afa1a..69802efb 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java +++ b/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java @@ -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. * - *

Only an entry with a non-blank {@code tab} is in scope: one with no {@code tab} feeds + *

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 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."); } /** diff --git a/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java b/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java index 6bc4216c..838268ba 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java @@ -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 -- 2.52.0 From f50a150a4dc05e825add17b1f2cebd1a1a545683 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Mon, 5 Oct 2026 14:13:32 +0200 Subject: [PATCH 2/2] fleetd #775: say which remedy applies to a lead vs a collaborator The ticket's correction comment pointed out the refusal message still implied removing a lead's tab helps, when only placement: tab does. Restate the message so the lead and collaborator remedies are not conflated, and keep the variable name the correction specified. --- .../src/main/java/dev/ltms/fleet/config/FleetConfig.java | 9 +++++---- .../test/java/dev/ltms/fleet/config/FleetConfigTest.java | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java b/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java index 69802efb..5827df74 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java +++ b/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java @@ -2978,10 +2978,10 @@ public record FleetConfig( if (fleet == null) { return; } - boolean anyLeaderExists = !fleet.leaders().isEmpty(); + boolean anyLeaderHasTab = !fleet.leaders().isEmpty(); boolean anyCollaboratorHasTab = fleet.collaborators().values().stream() .anyMatch(c -> c != null && c.tab() != null && !c.tab().isBlank()); - if (!anyLeaderExists && !anyCollaboratorHasTab) { + if (!anyLeaderHasTab && !anyCollaboratorHasTab) { return; } List bad = new ArrayList<>(); @@ -2998,8 +2998,9 @@ 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 fleet.leaders entry, or remove the tab from " - + "each fleet.collaborators entry."); + + "each named profile — the only fix when a lead triggered this, since a lead's " + + "tab label is fixed regardless of its own tab: field. A collaborator's tab can " + + "still be removed instead."); } /** diff --git a/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java b/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java index 838268ba..fdfc1d88 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java @@ -946,8 +946,8 @@ class FleetConfigTest { 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"); + assertTrue(e.getMessage().contains("the only fix when a lead triggered this"), + "the message must say placement: tab is the only fix for a lead"); 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"); } -- 2.52.0