Files
gastown/internal/polecat/namepool_test.go
Steve Yegge 97e0535bfe Implement three-tier config architecture (gt-k1lr tasks 1-5)
**Architecture changes:**
- Renamed `.gastown/` → `.runtime/` for runtime state (gitignored)
- Added `settings/` directory for rig behavioral config (git-tracked)
- Added `mayor/config.json` for town-level config (MayorConfig type)
- Separated RigConfig (identity) from RigSettings (behavioral)

**File location changes:**
- Town runtime: `~/.gastown/*` → `~/.runtime/*`
- Rig runtime: `<rig>/.gastown/*` → `<rig>/.runtime/*`
- Rig config: `<rig>/.gastown/config.json` → `<rig>/settings/config.json`
- Namepool state: `namepool.json` → `namepool-state.json`

**New types:**
- MayorConfig: town-level behavioral config
- RigSettings: rig behavioral config (merge_queue, theme, namepool)
- RigConfig now identity-only (name, git_url, beads, created_at)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 01:22:43 -08:00

433 lines
9.7 KiB
Go

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