package polecat import ( "os" "path/filepath" "testing" ) func TestNamePool_Allocate(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") // First allocation should be first themed name (furiosa) name, err := pool.Allocate() if err != nil { t.Fatalf("Allocate error: %v", err) } if name != "furiosa" { t.Errorf("expected furiosa, got %s", name) } // Second allocation should be nux name, err = pool.Allocate() if err != nil { t.Fatalf("Allocate error: %v", err) } if name != "nux" { t.Errorf("expected nux, got %s", name) } } func TestNamePool_Release(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") // Allocate first two name1, _ := pool.Allocate() name2, _ := pool.Allocate() if name1 != "furiosa" || name2 != "nux" { t.Fatalf("unexpected allocations: %s, %s", name1, name2) } // Release first one pool.Release("furiosa") // Next allocation should reuse furiosa name, _ := pool.Allocate() if name != "furiosa" { t.Errorf("expected furiosa to be reused, got %s", name) } } func TestNamePool_PrefersOrder(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") // Allocate first 5 for i := 0; i < 5; i++ { pool.Allocate() } // Release slit and furiosa pool.Release("slit") pool.Release("furiosa") // Next allocation should be furiosa (first in theme order) name, _ := pool.Allocate() if name != "furiosa" { t.Errorf("expected furiosa (first in order), got %s", name) } // Next should be slit name, _ = pool.Allocate() if name != "slit" { t.Errorf("expected slit, got %s", name) } } func TestNamePool_Overflow(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePoolWithConfig(tmpDir, "gastown", "mad-max", nil, 5) // Exhaust the small pool for i := 0; i < 5; i++ { pool.Allocate() } // Next allocation should be overflow format name, err := pool.Allocate() if err != nil { t.Fatalf("Allocate error: %v", err) } expected := "gastown-6" if name != expected { t.Errorf("expected overflow name %s, got %s", expected, name) } // Next overflow name, _ = pool.Allocate() if name != "gastown-7" { t.Errorf("expected gastown-7, got %s", name) } } func TestNamePool_OverflowNotReusable(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePoolWithConfig(tmpDir, "gastown", "mad-max", nil, 3) // Exhaust the pool for i := 0; i < 3; i++ { pool.Allocate() } // Get overflow name overflow1, _ := pool.Allocate() if overflow1 != "gastown-4" { t.Fatalf("expected gastown-4, got %s", overflow1) } // Release it - should not be reused pool.Release(overflow1) // Next allocation should be gastown-5, not gastown-4 name, _ := pool.Allocate() if name != "gastown-5" { t.Errorf("expected gastown-5 (overflow increments), got %s", name) } } func TestNamePool_SaveLoad(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") // Allocate some names pool.Allocate() // furiosa pool.Allocate() // nux pool.Allocate() // slit pool.Release("nux") // Save state if err := pool.Save(); err != nil { t.Fatalf("Save error: %v", err) } // Create new pool and load pool2 := NewNamePool(tmpDir, "testrig") if err := pool2.Load(); err != nil { t.Fatalf("Load error: %v", err) } // Should have furiosa and slit in use if pool2.ActiveCount() != 2 { t.Errorf("expected 2 active, got %d", pool2.ActiveCount()) } // Next allocation should be nux (released slot) name, _ := pool2.Allocate() if name != "nux" { t.Errorf("expected nux, got %s", name) } } func TestNamePool_Reconcile(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") // Simulate existing polecats from filesystem existing := []string{"slit", "valkyrie", "some-other-name"} pool.Reconcile(existing) if pool.ActiveCount() != 2 { t.Errorf("expected 2 active after reconcile, got %d", pool.ActiveCount()) } // Should allocate furiosa first (not slit or valkyrie) name, _ := pool.Allocate() if name != "furiosa" { t.Errorf("expected furiosa, got %s", name) } } func TestNamePool_IsPoolName(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") tests := []struct { name string expected bool }{ {"furiosa", true}, {"nux", true}, {"max", true}, {"gastown-51", false}, // overflow format {"random-name", false}, {"polecat-01", false}, // old format } for _, tc := range tests { result := pool.IsPoolName(tc.name) if result != tc.expected { t.Errorf("IsPoolName(%q) = %v, expected %v", tc.name, result, tc.expected) } } } func TestNamePool_ActiveNames(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") pool.Allocate() // furiosa pool.Allocate() // nux pool.Allocate() // slit pool.Release("nux") names := pool.ActiveNames() if len(names) != 2 { t.Errorf("expected 2 active names, got %d", len(names)) } // Names are sorted if names[0] != "furiosa" || names[1] != "slit" { t.Errorf("expected [furiosa, slit], got %v", names) } } func TestNamePool_MarkInUse(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") // Mark some slots as in use pool.MarkInUse("dementus") pool.MarkInUse("valkyrie") // Allocate should skip those name, _ := pool.Allocate() if name != "furiosa" { t.Errorf("expected furiosa, got %s", name) } // Verify count if pool.ActiveCount() != 3 { // furiosa, dementus, valkyrie t.Errorf("expected 3 active, got %d", pool.ActiveCount()) } } func TestNamePool_StateFilePath(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") pool.Allocate() if err := pool.Save(); err != nil { t.Fatalf("Save error: %v", err) } // Verify file was created in expected location expectedPath := filepath.Join(tmpDir, ".runtime", "namepool-state.json") if _, err := os.Stat(expectedPath); err != nil { t.Errorf("state file not found at expected path: %v", err) } } func TestNamePool_Themes(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() // Test minerals theme pool := NewNamePoolWithConfig(tmpDir, "testrig", "minerals", nil, 50) name, err := pool.Allocate() if err != nil { t.Fatalf("Allocate error: %v", err) } if name != "obsidian" { t.Errorf("expected obsidian (first mineral), got %s", name) } // Test theme switching if err := pool.SetTheme("wasteland"); err != nil { t.Fatalf("SetTheme error: %v", err) } // obsidian should be released (not in wasteland theme) name, _ = pool.Allocate() if name != "rust" { t.Errorf("expected rust (first wasteland name), got %s", name) } } func TestNamePool_CustomNames(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() custom := []string{"alpha", "beta", "gamma", "delta"} pool := NewNamePoolWithConfig(tmpDir, "testrig", "", custom, 4) name, _ := pool.Allocate() if name != "alpha" { t.Errorf("expected alpha, got %s", name) } name, _ = pool.Allocate() if name != "beta" { t.Errorf("expected beta, got %s", name) } } func TestListThemes(t *testing.T) { themes := ListThemes() if len(themes) != 3 { t.Errorf("expected 3 themes, got %d", len(themes)) } // Check that all expected themes are present expected := map[string]bool{"mad-max": true, "minerals": true, "wasteland": true} for _, theme := range themes { if !expected[theme] { t.Errorf("unexpected theme: %s", theme) } } } func TestGetThemeNames(t *testing.T) { names, err := GetThemeNames("mad-max") if err != nil { t.Fatalf("GetThemeNames error: %v", err) } if len(names) != 50 { t.Errorf("expected 50 mad-max names, got %d", len(names)) } if names[0] != "furiosa" { t.Errorf("expected first name to be furiosa, got %s", names[0]) } // Test invalid theme _, err = GetThemeNames("invalid-theme") if err == nil { t.Error("expected error for invalid theme") } } func TestNamePool_Reset(t *testing.T) { tmpDir, err := os.MkdirTemp("", "namepool-test-*") if err != nil { t.Fatal(err) } defer func() { _ = os.RemoveAll(tmpDir) }() pool := NewNamePool(tmpDir, "testrig") // Allocate several names for i := 0; i < 10; i++ { pool.Allocate() } if pool.ActiveCount() != 10 { t.Errorf("expected 10 active, got %d", pool.ActiveCount()) } // Reset pool.Reset() if pool.ActiveCount() != 0 { t.Errorf("expected 0 active after reset, got %d", pool.ActiveCount()) } // Should allocate furiosa again name, _ := pool.Allocate() if name != "furiosa" { t.Errorf("expected furiosa after reset, got %s", name) } }