From 3afd1a1dcd451851f43d58453fb029c5b333a880 Mon Sep 17 00:00:00 2001 From: Advaya Krishna Date: Wed, 21 Jan 2026 10:30:53 -0800 Subject: [PATCH] fix(polecat): exclude reserved infrastructure agent names from allocator (#837) The polecat name allocator was assigning reserved infrastructure agent names like 'witness' to polecats. Added ReservedInfraAgentNames map containing witness, mayor, deacon, and refinery. Modified getNames() to filter these from all themes and custom name lists. Co-authored-by: Claude Opus 4.5 --- internal/polecat/namepool.go | 38 +++++++++++++++---- internal/polecat/namepool_test.go | 63 +++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/internal/polecat/namepool.go b/internal/polecat/namepool.go index ade0be3d..eb8d8efa 100644 --- a/internal/polecat/namepool.go +++ b/internal/polecat/namepool.go @@ -22,6 +22,15 @@ const ( DefaultTheme = "mad-max" ) +// ReservedInfraAgentNames contains names reserved for infrastructure agents. +// These names must never be allocated to polecats. +var ReservedInfraAgentNames = map[string]bool{ + "witness": true, + "mayor": true, + "deacon": true, + "refinery": true, +} + // Built-in themes with themed polecat names. var BuiltinThemes = map[string][]string{ "mad-max": { @@ -132,19 +141,34 @@ func NewNamePoolWithConfig(rigPath, rigName, theme string, customNames []string, } // getNames returns the list of names to use for the pool. +// Reserved infrastructure agent names are filtered out. func (p *NamePool) getNames() []string { + var names []string + // Custom names take precedence if len(p.CustomNames) > 0 { - return p.CustomNames + names = p.CustomNames + } else if themeNames, ok := BuiltinThemes[p.Theme]; ok { + // Look up built-in theme + names = themeNames + } else { + // Fall back to default theme + names = BuiltinThemes[DefaultTheme] } - // Look up built-in theme - if names, ok := BuiltinThemes[p.Theme]; ok { - return names - } + // Filter out reserved infrastructure agent names + return filterReservedNames(names) +} - // Fall back to default theme - return BuiltinThemes[DefaultTheme] +// filterReservedNames removes reserved infrastructure agent names from a name list. +func filterReservedNames(names []string) []string { + filtered := make([]string, 0, len(names)) + for _, name := range names { + if !ReservedInfraAgentNames[name] { + filtered = append(filtered, name) + } + } + return filtered } // Load loads the pool state from disk. diff --git a/internal/polecat/namepool_test.go b/internal/polecat/namepool_test.go index 0b447887..051abd66 100644 --- a/internal/polecat/namepool_test.go +++ b/internal/polecat/namepool_test.go @@ -462,3 +462,66 @@ func TestThemeForRigDeterministic(t *testing.T) { t.Errorf("theme not deterministic: got %q and %q", theme1, theme2) } } + +func TestNamePool_ReservedNamesExcluded(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "namepool-test-*") + if err != nil { + t.Fatal(err) + } + defer func() { _ = os.RemoveAll(tmpDir) }() + + // Test all themes to ensure reserved names are excluded + for themeName := range BuiltinThemes { + pool := NewNamePoolWithConfig(tmpDir, "testrig", themeName, nil, 100) + + // Allocate all available names (up to 100) + allocated := make(map[string]bool) + for i := 0; i < 100; i++ { + name, err := pool.Allocate() + if err != nil { + t.Fatalf("Allocate error: %v", err) + } + allocated[name] = true + } + + // Verify no reserved names were allocated + for reserved := range ReservedInfraAgentNames { + if allocated[reserved] { + t.Errorf("theme %q allocated reserved name %q", themeName, reserved) + } + } + + pool.Reset() + } +} + +func TestNamePool_ReservedNamesInCustomNames(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "namepool-test-*") + if err != nil { + t.Fatal(err) + } + defer func() { _ = os.RemoveAll(tmpDir) }() + + // Custom names that include reserved names should have them filtered out + custom := []string{"alpha", "witness", "beta", "mayor", "gamma"} + pool := NewNamePoolWithConfig(tmpDir, "testrig", "", custom, 10) + + // Allocate all names + allocated := make(map[string]bool) + for i := 0; i < 5; i++ { + name, _ := pool.Allocate() + allocated[name] = true + } + + // Should only get alpha, beta, gamma (3 non-reserved names) + // Then overflow names for the remaining allocations + if allocated["witness"] { + t.Error("allocated reserved name 'witness' from custom names") + } + if allocated["mayor"] { + t.Error("allocated reserved name 'mayor' from custom names") + } + if !allocated["alpha"] || !allocated["beta"] || !allocated["gamma"] { + t.Errorf("expected alpha, beta, gamma to be allocated, got %v", allocated) + } +}