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) + } +}