**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>
433 lines
9.7 KiB
Go
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)
|
|
}
|
|
}
|