feat(namepool): Add themed name pools for polecats

Polecats now get themed names from the Mad Max universe by default
(furiosa, nux, slit, etc.) instead of generic polecat-01, polecat-02.

Changes:
- Add NamepoolConfig to config/types.go for per-rig theme configuration
- Update namepool.go with three built-in themes:
  - mad-max (default): furiosa, nux, imperator, etc.
  - minerals: obsidian, quartz, ruby, etc.
  - wasteland: rust, chrome, fury, etc.
- Add gt namepool commands: themes, set, add, reset
- Update manager.go to load namepool config from rig settings

Configuration in .gastown/config.json:
```json
{
  "namepool": {
    "style": "minerals",
    "max_before_numbering": 50
  }
}
```

Issue: beads-rs0

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-19 21:59:03 -08:00
parent c7e83b1619
commit 900a440ce8
6 changed files with 753 additions and 121 deletions

310
internal/cmd/namepool.go Normal file
View File

@@ -0,0 +1,310 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/polecat"
"github.com/steveyegge/gastown/internal/workspace"
)
var (
namepoolListFlag bool
namepoolThemeFlag string
)
var namepoolCmd = &cobra.Command{
Use: "namepool",
Short: "Manage polecat name pools",
Long: `Manage themed name pools for polecats in Gas Town.
By default, polecats get themed names from the Mad Max universe
(furiosa, nux, slit, etc.). You can change the theme or add custom names.
Examples:
gt namepool # Show current pool status
gt namepool --list # List available themes
gt namepool themes # Show theme names
gt namepool set minerals # Set theme to 'minerals'
gt namepool add ember # Add custom name to pool
gt namepool reset # Reset pool state`,
RunE: runNamepool,
}
var namepoolThemesCmd = &cobra.Command{
Use: "themes [theme]",
Short: "List available themes and their names",
RunE: runNamepoolThemes,
}
var namepoolSetCmd = &cobra.Command{
Use: "set <theme>",
Short: "Set the namepool theme for this rig",
Args: cobra.ExactArgs(1),
RunE: runNamepoolSet,
}
var namepoolAddCmd = &cobra.Command{
Use: "add <name>",
Short: "Add a custom name to the pool",
Args: cobra.ExactArgs(1),
RunE: runNamepoolAdd,
}
var namepoolResetCmd = &cobra.Command{
Use: "reset",
Short: "Reset the pool state (release all names)",
RunE: runNamepoolReset,
}
func init() {
rootCmd.AddCommand(namepoolCmd)
namepoolCmd.AddCommand(namepoolThemesCmd)
namepoolCmd.AddCommand(namepoolSetCmd)
namepoolCmd.AddCommand(namepoolAddCmd)
namepoolCmd.AddCommand(namepoolResetCmd)
namepoolCmd.Flags().BoolVarP(&namepoolListFlag, "list", "l", false, "List available themes")
}
func runNamepool(cmd *cobra.Command, args []string) error {
// List themes mode
if namepoolListFlag {
return runNamepoolThemes(cmd, nil)
}
// Show current pool status
rigName, rigPath := detectCurrentRigWithPath()
if rigName == "" {
return fmt.Errorf("not in a rig directory")
}
// Load pool
pool := polecat.NewNamePool(rigPath, rigName)
if err := pool.Load(); err != nil {
// Pool doesn't exist yet, show defaults
fmt.Printf("Rig: %s\n", rigName)
fmt.Printf("Theme: %s (default)\n", polecat.DefaultTheme)
fmt.Printf("Active polecats: 0\n")
fmt.Printf("Max pool size: %d\n", polecat.DefaultPoolSize)
return nil
}
// Show pool status
fmt.Printf("Rig: %s\n", rigName)
fmt.Printf("Theme: %s\n", pool.GetTheme())
fmt.Printf("Active polecats: %d\n", pool.ActiveCount())
activeNames := pool.ActiveNames()
if len(activeNames) > 0 {
fmt.Printf("In use: %s\n", strings.Join(activeNames, ", "))
}
// Check if configured
configPath := filepath.Join(rigPath, ".gastown", "config.json")
if cfg, err := config.LoadRigConfig(configPath); err == nil && cfg.Namepool != nil {
fmt.Printf("(configured in .gastown/config.json)\n")
}
return nil
}
func runNamepoolThemes(cmd *cobra.Command, args []string) error {
themes := polecat.ListThemes()
if len(args) == 0 {
// List all themes
fmt.Println("Available themes:")
for _, theme := range themes {
names, _ := polecat.GetThemeNames(theme)
fmt.Printf("\n %s (%d names):\n", theme, len(names))
// Show first 10 names
preview := names
if len(preview) > 10 {
preview = preview[:10]
}
fmt.Printf(" %s...\n", strings.Join(preview, ", "))
}
return nil
}
// Show specific theme names
theme := args[0]
names, err := polecat.GetThemeNames(theme)
if err != nil {
return fmt.Errorf("unknown theme: %s (available: %s)", theme, strings.Join(themes, ", "))
}
fmt.Printf("Theme: %s (%d names)\n\n", theme, len(names))
for i, name := range names {
if i > 0 && i%5 == 0 {
fmt.Println()
}
fmt.Printf(" %-12s", name)
}
fmt.Println()
return nil
}
func runNamepoolSet(cmd *cobra.Command, args []string) error {
theme := args[0]
// Validate theme
themes := polecat.ListThemes()
valid := false
for _, t := range themes {
if t == theme {
valid = true
break
}
}
if !valid {
return fmt.Errorf("unknown theme: %s (available: %s)", theme, strings.Join(themes, ", "))
}
// Get rig
rigName, rigPath := detectCurrentRigWithPath()
if rigName == "" {
return fmt.Errorf("not in a rig directory")
}
// Update pool
pool := polecat.NewNamePool(rigPath, rigName)
if err := pool.Load(); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("loading pool: %w", err)
}
if err := pool.SetTheme(theme); err != nil {
return err
}
if err := pool.Save(); err != nil {
return fmt.Errorf("saving pool: %w", err)
}
// Also save to rig config
if err := saveRigNamepoolConfig(rigPath, theme, nil); err != nil {
return fmt.Errorf("saving config: %w", err)
}
fmt.Printf("Theme '%s' set for rig '%s'\n", theme, rigName)
fmt.Printf("New polecats will use names from this theme.\n")
return nil
}
func runNamepoolAdd(cmd *cobra.Command, args []string) error {
name := args[0]
rigName, rigPath := detectCurrentRigWithPath()
if rigName == "" {
return fmt.Errorf("not in a rig directory")
}
// Load pool
pool := polecat.NewNamePool(rigPath, rigName)
if err := pool.Load(); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("loading pool: %w", err)
}
pool.AddCustomName(name)
if err := pool.Save(); err != nil {
return fmt.Errorf("saving pool: %w", err)
}
fmt.Printf("Added '%s' to the name pool\n", name)
return nil
}
func runNamepoolReset(cmd *cobra.Command, args []string) error {
rigName, rigPath := detectCurrentRigWithPath()
if rigName == "" {
return fmt.Errorf("not in a rig directory")
}
// Load pool
pool := polecat.NewNamePool(rigPath, rigName)
if err := pool.Load(); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("loading pool: %w", err)
}
pool.Reset()
if err := pool.Save(); err != nil {
return fmt.Errorf("saving pool: %w", err)
}
fmt.Printf("Pool reset for rig '%s'\n", rigName)
fmt.Printf("All names released and available for reuse.\n")
return nil
}
// detectCurrentRigWithPath determines the rig name and path from cwd.
func detectCurrentRigWithPath() (string, string) {
cwd, err := os.Getwd()
if err != nil {
return "", ""
}
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
return "", ""
}
// Get path relative to town root
rel, err := filepath.Rel(townRoot, cwd)
if err != nil {
return "", ""
}
// Extract first path component (rig name)
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) > 0 && parts[0] != "." && parts[0] != "mayor" && parts[0] != "deacon" {
return parts[0], filepath.Join(townRoot, parts[0])
}
return "", ""
}
// saveRigNamepoolConfig saves the namepool config to rig config.
func saveRigNamepoolConfig(rigPath, theme string, customNames []string) error {
configPath := filepath.Join(rigPath, ".gastown", "config.json")
// Load existing config or create new
var cfg *config.RigConfig
cfg, err := config.LoadRigConfig(configPath)
if err != nil {
// Create new config if not found
if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") {
cfg = &config.RigConfig{
Type: "rig",
Version: config.CurrentRigConfigVersion,
}
} else {
return fmt.Errorf("loading config: %w", err)
}
}
// Set namepool
cfg.Namepool = &config.NamepoolConfig{
Style: theme,
Names: customNames,
}
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return err
}
// Save
if err := config.SaveRigConfig(configPath, cfg); err != nil {
return fmt.Errorf("saving config: %w", err)
}
return nil
}

View File

@@ -53,6 +53,7 @@ type RigConfig struct {
Version int `json:"version"` // schema version
MergeQueue *MergeQueueConfig `json:"merge_queue,omitempty"` // merge queue settings
Theme *ThemeConfig `json:"theme,omitempty"` // tmux theme settings
Namepool *NamepoolConfig `json:"namepool,omitempty"` // polecat name pool settings
}
// ThemeConfig represents tmux theme settings for a rig.
@@ -125,3 +126,26 @@ func DefaultMergeQueueConfig() *MergeQueueConfig {
MaxConcurrent: 1,
}
}
// NamepoolConfig represents namepool settings for themed polecat names.
type NamepoolConfig struct {
// Style picks from a built-in theme (e.g., "mad-max", "minerals", "wasteland").
// If empty, defaults to "mad-max".
Style string `json:"style,omitempty"`
// Names is a custom list of names to use instead of a built-in theme.
// If provided, overrides the Style setting.
Names []string `json:"names,omitempty"`
// MaxBeforeNumbering is when to start appending numbers.
// Default is 50. After this many polecats, names become name-01, name-02, etc.
MaxBeforeNumbering int `json:"max_before_numbering,omitempty"`
}
// DefaultNamepoolConfig returns a NamepoolConfig with sensible defaults.
func DefaultNamepoolConfig() *NamepoolConfig {
return &NamepoolConfig{
Style: "mad-max",
MaxBeforeNumbering: 50,
}
}

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/rig"
)
@@ -32,8 +33,24 @@ func NewManager(r *rig.Rig, g *git.Git) *Manager {
// Use the mayor's rig directory for beads operations (rig-level beads)
mayorRigPath := filepath.Join(r.Path, "mayor", "rig")
// Initialize name pool
pool := NewNamePool(r.Path, r.Name)
// Try to load rig config for namepool settings
rigConfigPath := filepath.Join(r.Path, ".gastown", "config.json")
var pool *NamePool
rigConfig, err := config.LoadRigConfig(rigConfigPath)
if err == nil && rigConfig.Namepool != nil {
// Use configured namepool settings
pool = NewNamePoolWithConfig(
r.Path,
r.Name,
rigConfig.Namepool.Style,
rigConfig.Namepool.Names,
rigConfig.Namepool.MaxBeforeNumbering,
)
} else {
// Use defaults
pool = NewNamePool(r.Path, r.Name)
}
_ = pool.Load() // Load existing state, ignore errors for new rigs
return &Manager{

View File

@@ -10,15 +10,55 @@ import (
)
const (
// PoolSize is the number of reusable names in the pool.
PoolSize = 50
// DefaultPoolSize is the number of reusable names in the pool.
DefaultPoolSize = 50
// NamePrefix is the prefix for pooled polecat names.
NamePrefix = "polecat-"
// DefaultTheme is the default theme for new rigs.
DefaultTheme = "mad-max"
)
// Built-in themes with themed polecat names.
var BuiltinThemes = map[string][]string{
"mad-max": {
"furiosa", "nux", "slit", "rictus", "dementus",
"capable", "toast", "dag", "cheedo", "valkyrie",
"keeper", "morsov", "ace", "warboy", "imperator",
"organic", "coma", "splendid", "angharad", "max",
"immortan", "bullet", "toecutter", "goose", "nightrider",
"glory", "scrotus", "chumbucket", "corpus", "dinki",
"prime", "vuvalini", "rockryder", "wretched", "buzzard",
"gastown", "bullet-farmer", "citadel", "wasteland", "fury",
"road-warrior", "interceptor", "blackfinger", "wraith", "witness",
"chrome", "shiny", "mediocre", "guzzoline", "aqua-cola",
},
"minerals": {
"obsidian", "quartz", "jasper", "onyx", "opal",
"topaz", "garnet", "ruby", "amber", "jade",
"pearl", "flint", "granite", "basalt", "marble",
"shale", "slate", "pyrite", "mica", "agate",
"malachite", "turquoise", "lapis", "emerald", "sapphire",
"diamond", "amethyst", "citrine", "zircon", "peridot",
"coral", "jet", "moonstone", "sunstone", "bloodstone",
"rhodonite", "sodalite", "hematite", "magnetite", "calcite",
"fluorite", "selenite", "kyanite", "labradorite", "amazonite",
"chalcedony", "carnelian", "aventurine", "chrysoprase", "heliodor",
},
"wasteland": {
"rust", "chrome", "nitro", "guzzle", "witness",
"shiny", "fury", "thunder", "dust", "scavenger",
"radrat", "ghoul", "mutant", "raider", "vault",
"pipboy", "nuka", "brahmin", "deathclaw", "mirelurk",
"synth", "institute", "enclave", "brotherhood", "minuteman",
"railroad", "atom", "crater", "foundation", "refuge",
"settler", "wanderer", "courier", "lone", "chosen",
"tribal", "khan", "legion", "ncr", "ranger",
"overseer", "sentinel", "paladin", "scribe", "initiate",
"elder", "lancer", "knight", "squire", "proctor",
},
}
// NamePool manages a bounded pool of reusable polecat names.
// Names in the pool are polecat-01 through polecat-50.
// Names are drawn from a themed pool (mad-max by default).
// When the pool is exhausted, overflow names use rigname-N format.
type NamePool struct {
mu sync.RWMutex
@@ -26,14 +66,23 @@ type NamePool struct {
// RigName is the rig this pool belongs to.
RigName string `json:"rig_name"`
// InUse tracks which pool indices are currently in use.
// Key is the pool index (1-50), value is true if in use.
InUse map[int]bool `json:"in_use"`
// Theme is the current theme name (e.g., "mad-max", "minerals").
Theme string `json:"theme"`
// CustomNames allows overriding the built-in theme names.
CustomNames []string `json:"custom_names,omitempty"`
// InUse tracks which pool names are currently in use.
// Key is the name itself, value is true if in use.
InUse map[string]bool `json:"in_use"`
// OverflowNext is the next overflow sequence number.
// Starts at PoolSize+1 (51) and increments.
// Starts at MaxSize+1 and increments.
OverflowNext int `json:"overflow_next"`
// MaxSize is the maximum number of themed names before overflow.
MaxSize int `json:"max_size"`
// stateFile is the path to persist pool state.
stateFile string
}
@@ -42,12 +91,50 @@ type NamePool struct {
func NewNamePool(rigPath, rigName string) *NamePool {
return &NamePool{
RigName: rigName,
InUse: make(map[int]bool),
OverflowNext: PoolSize + 1,
Theme: DefaultTheme,
InUse: make(map[string]bool),
OverflowNext: DefaultPoolSize + 1,
MaxSize: DefaultPoolSize,
stateFile: filepath.Join(rigPath, ".gastown", "namepool.json"),
}
}
// NewNamePoolWithConfig creates a name pool with specific configuration.
func NewNamePoolWithConfig(rigPath, rigName, theme string, customNames []string, maxSize int) *NamePool {
if theme == "" {
theme = DefaultTheme
}
if maxSize <= 0 {
maxSize = DefaultPoolSize
}
return &NamePool{
RigName: rigName,
Theme: theme,
CustomNames: customNames,
InUse: make(map[string]bool),
OverflowNext: maxSize + 1,
MaxSize: maxSize,
stateFile: filepath.Join(rigPath, ".gastown", "namepool.json"),
}
}
// getNames returns the list of names to use for the pool.
func (p *NamePool) getNames() []string {
// Custom names take precedence
if len(p.CustomNames) > 0 {
return p.CustomNames
}
// Look up built-in theme
if names, ok := BuiltinThemes[p.Theme]; ok {
return names
}
// Fall back to default theme
return BuiltinThemes[DefaultTheme]
}
// Load loads the pool state from disk.
func (p *NamePool) Load() error {
p.mu.Lock()
@@ -57,8 +144,8 @@ func (p *NamePool) Load() error {
if err != nil {
if os.IsNotExist(err) {
// Initialize with empty state
p.InUse = make(map[int]bool)
p.OverflowNext = PoolSize + 1
p.InUse = make(map[string]bool)
p.OverflowNext = p.MaxSize + 1
return nil
}
return err
@@ -69,13 +156,24 @@ func (p *NamePool) Load() error {
return err
}
// Preserve the theme and custom names if already set
if p.Theme == "" && loaded.Theme != "" {
p.Theme = loaded.Theme
}
if len(p.CustomNames) == 0 && len(loaded.CustomNames) > 0 {
p.CustomNames = loaded.CustomNames
}
p.InUse = loaded.InUse
if p.InUse == nil {
p.InUse = make(map[int]bool)
p.InUse = make(map[string]bool)
}
p.OverflowNext = loaded.OverflowNext
if p.OverflowNext < PoolSize+1 {
p.OverflowNext = PoolSize + 1
if p.OverflowNext < p.MaxSize+1 {
p.OverflowNext = p.MaxSize + 1
}
if loaded.MaxSize > 0 {
p.MaxSize = loaded.MaxSize
}
return nil
@@ -100,17 +198,20 @@ func (p *NamePool) Save() error {
}
// Allocate returns a name from the pool.
// It prefers lower-numbered pool slots, and falls back to overflow names
// It prefers names in order from the theme list, and falls back to overflow names
// when the pool is exhausted.
func (p *NamePool) Allocate() (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
// Try to find first available slot in pool (prefer low numbers)
for i := 1; i <= PoolSize; i++ {
if !p.InUse[i] {
p.InUse[i] = true
return p.formatPoolName(i), nil
names := p.getNames()
// Try to find first available name from the theme
for i := 0; i < len(names) && i < p.MaxSize; i++ {
name := names[i]
if !p.InUse[name] {
p.InUse[name] = true
return name, nil
}
}
@@ -126,17 +227,27 @@ func (p *NamePool) Release(name string) {
p.mu.Lock()
defer p.mu.Unlock()
idx := p.parsePoolIndex(name)
if idx > 0 && idx <= PoolSize {
delete(p.InUse, idx)
// Check if it's a themed name
if p.isThemedName(name) {
delete(p.InUse, name)
}
// Overflow names are not reusable, so we don't track them
}
// IsPoolName returns true if the name is a pool name (polecat-NN format).
// isThemedName checks if a name is in the theme pool.
func (p *NamePool) isThemedName(name string) bool {
names := p.getNames()
for _, n := range names {
if n == name {
return true
}
}
return false
}
// IsPoolName returns true if the name is a pool name (themed or numbered).
func (p *NamePool) IsPoolName(name string) bool {
idx := p.parsePoolIndex(name)
return idx > 0 && idx <= PoolSize
return p.isThemedName(name)
}
// ActiveCount returns the number of names currently in use from the pool.
@@ -152,8 +263,8 @@ func (p *NamePool) ActiveNames() []string {
defer p.mu.RUnlock()
var names []string
for idx := range p.InUse {
names = append(names, p.formatPoolName(idx))
for name := range p.InUse {
names = append(names, name)
}
sort.Strings(names)
return names
@@ -164,9 +275,8 @@ func (p *NamePool) MarkInUse(name string) {
p.mu.Lock()
defer p.mu.Unlock()
idx := p.parsePoolIndex(name)
if idx > 0 && idx <= PoolSize {
p.InUse[idx] = true
if p.isThemedName(name) {
p.InUse[name] = true
}
}
@@ -177,41 +287,93 @@ func (p *NamePool) Reconcile(existingPolecats []string) {
defer p.mu.Unlock()
// Clear current state
p.InUse = make(map[int]bool)
p.InUse = make(map[string]bool)
// Mark all existing polecats as in use
for _, name := range existingPolecats {
idx := p.parsePoolIndex(name)
if idx > 0 && idx <= PoolSize {
p.InUse[idx] = true
if p.isThemedName(name) {
p.InUse[name] = true
}
}
}
// formatPoolName formats a pool index as a name.
func (p *NamePool) formatPoolName(idx int) string {
return fmt.Sprintf("%s%02d", NamePrefix, idx)
}
// formatOverflowName formats an overflow sequence number as a name.
func (p *NamePool) formatOverflowName(seq int) string {
return fmt.Sprintf("%s-%d", p.RigName, seq)
}
// parsePoolIndex extracts the pool index from a pool name.
// Returns 0 if not a valid pool name.
func (p *NamePool) parsePoolIndex(name string) int {
if len(name) < len(NamePrefix)+2 {
return 0
}
if name[:len(NamePrefix)] != NamePrefix {
return 0
// GetTheme returns the current theme name.
func (p *NamePool) GetTheme() string {
p.mu.RLock()
defer p.mu.RUnlock()
return p.Theme
}
// SetTheme sets the theme and resets the pool.
// Existing in-use names are preserved if they exist in the new theme.
func (p *NamePool) SetTheme(theme string) error {
p.mu.Lock()
defer p.mu.Unlock()
if _, ok := BuiltinThemes[theme]; !ok {
return fmt.Errorf("unknown theme: %s (available: mad-max, minerals, wasteland)", theme)
}
var idx int
_, err := fmt.Sscanf(name[len(NamePrefix):], "%d", &idx)
if err != nil {
return 0
// Preserve names that exist in both themes
newNames := BuiltinThemes[theme]
newInUse := make(map[string]bool)
for name := range p.InUse {
for _, n := range newNames {
if n == name {
newInUse[name] = true
break
}
}
}
return idx
p.Theme = theme
p.InUse = newInUse
p.CustomNames = nil
return nil
}
// ListThemes returns the list of available built-in themes.
func ListThemes() []string {
themes := make([]string, 0, len(BuiltinThemes))
for theme := range BuiltinThemes {
themes = append(themes, theme)
}
sort.Strings(themes)
return themes
}
// GetThemeNames returns the names in a specific theme.
func GetThemeNames(theme string) ([]string, error) {
if names, ok := BuiltinThemes[theme]; ok {
return names, nil
}
return nil, fmt.Errorf("unknown theme: %s", theme)
}
// AddCustomName adds a custom name to the pool.
func (p *NamePool) AddCustomName(name string) {
p.mu.Lock()
defer p.mu.Unlock()
// Check if already in custom names
for _, n := range p.CustomNames {
if n == name {
return
}
}
p.CustomNames = append(p.CustomNames, name)
}
// Reset clears the pool state, releasing all names.
func (p *NamePool) Reset() {
p.mu.Lock()
defer p.mu.Unlock()
p.InUse = make(map[string]bool)
p.OverflowNext = p.MaxSize + 1
}

View File

@@ -15,22 +15,22 @@ func TestNamePool_Allocate(t *testing.T) {
pool := NewNamePool(tmpDir, "testrig")
// First allocation should be polecat-01
// First allocation should be first themed name (furiosa)
name, err := pool.Allocate()
if err != nil {
t.Fatalf("Allocate error: %v", err)
}
if name != "polecat-01" {
t.Errorf("expected polecat-01, got %s", name)
if name != "furiosa" {
t.Errorf("expected furiosa, got %s", name)
}
// Second allocation should be polecat-02
// Second allocation should be nux
name, err = pool.Allocate()
if err != nil {
t.Fatalf("Allocate error: %v", err)
}
if name != "polecat-02" {
t.Errorf("expected polecat-02, got %s", name)
if name != "nux" {
t.Errorf("expected nux, got %s", name)
}
}
@@ -47,21 +47,21 @@ func TestNamePool_Release(t *testing.T) {
name1, _ := pool.Allocate()
name2, _ := pool.Allocate()
if name1 != "polecat-01" || name2 != "polecat-02" {
if name1 != "furiosa" || name2 != "nux" {
t.Fatalf("unexpected allocations: %s, %s", name1, name2)
}
// Release first one
pool.Release("polecat-01")
pool.Release("furiosa")
// Next allocation should reuse polecat-01
// Next allocation should reuse furiosa
name, _ := pool.Allocate()
if name != "polecat-01" {
t.Errorf("expected polecat-01 to be reused, got %s", name)
if name != "furiosa" {
t.Errorf("expected furiosa to be reused, got %s", name)
}
}
func TestNamePool_PrefersLowNumbers(t *testing.T) {
func TestNamePool_PrefersOrder(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "namepool-test-*")
if err != nil {
t.Fatal(err)
@@ -75,20 +75,20 @@ func TestNamePool_PrefersLowNumbers(t *testing.T) {
pool.Allocate()
}
// Release 03 and 01
pool.Release("polecat-03")
pool.Release("polecat-01")
// Release slit and furiosa
pool.Release("slit")
pool.Release("furiosa")
// Next allocation should be 01 (lowest available)
// Next allocation should be furiosa (first in theme order)
name, _ := pool.Allocate()
if name != "polecat-01" {
t.Errorf("expected polecat-01 (lowest), got %s", name)
if name != "furiosa" {
t.Errorf("expected furiosa (first in order), got %s", name)
}
// Next should be 03
// Next should be slit
name, _ = pool.Allocate()
if name != "polecat-03" {
t.Errorf("expected polecat-03, got %s", name)
if name != "slit" {
t.Errorf("expected slit, got %s", name)
}
}
@@ -99,10 +99,10 @@ func TestNamePool_Overflow(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "gastown")
pool := NewNamePoolWithConfig(tmpDir, "gastown", "mad-max", nil, 5)
// Exhaust the pool
for i := 0; i < PoolSize; i++ {
// Exhaust the small pool
for i := 0; i < 5; i++ {
pool.Allocate()
}
@@ -111,15 +111,15 @@ func TestNamePool_Overflow(t *testing.T) {
if err != nil {
t.Fatalf("Allocate error: %v", err)
}
expected := "gastown-51"
expected := "gastown-6"
if name != expected {
t.Errorf("expected overflow name %s, got %s", expected, name)
}
// Next overflow
name, _ = pool.Allocate()
if name != "gastown-52" {
t.Errorf("expected gastown-52, got %s", name)
if name != "gastown-7" {
t.Errorf("expected gastown-7, got %s", name)
}
}
@@ -130,26 +130,26 @@ func TestNamePool_OverflowNotReusable(t *testing.T) {
}
defer func() { _ = os.RemoveAll(tmpDir) }()
pool := NewNamePool(tmpDir, "gastown")
pool := NewNamePoolWithConfig(tmpDir, "gastown", "mad-max", nil, 3)
// Exhaust the pool
for i := 0; i < PoolSize; i++ {
for i := 0; i < 3; i++ {
pool.Allocate()
}
// Get overflow name
overflow1, _ := pool.Allocate()
if overflow1 != "gastown-51" {
t.Fatalf("expected gastown-51, got %s", overflow1)
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-52, not gastown-51
// Next allocation should be gastown-5, not gastown-4
name, _ := pool.Allocate()
if name != "gastown-52" {
t.Errorf("expected gastown-52 (overflow increments), got %s", name)
if name != "gastown-5" {
t.Errorf("expected gastown-5 (overflow increments), got %s", name)
}
}
@@ -163,10 +163,10 @@ func TestNamePool_SaveLoad(t *testing.T) {
pool := NewNamePool(tmpDir, "testrig")
// Allocate some names
pool.Allocate() // 01
pool.Allocate() // 02
pool.Allocate() // 03
pool.Release("polecat-02")
pool.Allocate() // furiosa
pool.Allocate() // nux
pool.Allocate() // slit
pool.Release("nux")
// Save state
if err := pool.Save(); err != nil {
@@ -179,15 +179,15 @@ func TestNamePool_SaveLoad(t *testing.T) {
t.Fatalf("Load error: %v", err)
}
// Should have 01 and 03 in use
// Should have furiosa and slit in use
if pool2.ActiveCount() != 2 {
t.Errorf("expected 2 active, got %d", pool2.ActiveCount())
}
// Next allocation should be 02 (released slot)
// Next allocation should be nux (released slot)
name, _ := pool2.Allocate()
if name != "polecat-02" {
t.Errorf("expected polecat-02, got %s", name)
if name != "nux" {
t.Errorf("expected nux, got %s", name)
}
}
@@ -201,7 +201,7 @@ func TestNamePool_Reconcile(t *testing.T) {
pool := NewNamePool(tmpDir, "testrig")
// Simulate existing polecats from filesystem
existing := []string{"polecat-03", "polecat-07", "some-other-name"}
existing := []string{"slit", "valkyrie", "some-other-name"}
pool.Reconcile(existing)
@@ -209,10 +209,10 @@ func TestNamePool_Reconcile(t *testing.T) {
t.Errorf("expected 2 active after reconcile, got %d", pool.ActiveCount())
}
// Should allocate 01 first (not 03 or 07)
// Should allocate furiosa first (not slit or valkyrie)
name, _ := pool.Allocate()
if name != "polecat-01" {
t.Errorf("expected polecat-01, got %s", name)
if name != "furiosa" {
t.Errorf("expected furiosa, got %s", name)
}
}
@@ -229,13 +229,12 @@ func TestNamePool_IsPoolName(t *testing.T) {
name string
expected bool
}{
{"polecat-01", true},
{"polecat-50", true},
{"polecat-51", false}, // > PoolSize
{"furiosa", true},
{"nux", true},
{"max", true},
{"gastown-51", false}, // overflow format
{"Nux", false}, // legacy name
{"polecat-", false}, // invalid
{"polecat-abc", false},
{"random-name", false},
{"polecat-01", false}, // old format
}
for _, tc := range tests {
@@ -255,17 +254,18 @@ func TestNamePool_ActiveNames(t *testing.T) {
pool := NewNamePool(tmpDir, "testrig")
pool.Allocate() // 01
pool.Allocate() // 02
pool.Allocate() // 03
pool.Release("polecat-02")
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))
}
if names[0] != "polecat-01" || names[1] != "polecat-03" {
t.Errorf("expected [polecat-01, polecat-03], got %v", names)
// Names are sorted
if names[0] != "furiosa" || names[1] != "slit" {
t.Errorf("expected [furiosa, slit], got %v", names)
}
}
@@ -279,17 +279,17 @@ func TestNamePool_MarkInUse(t *testing.T) {
pool := NewNamePool(tmpDir, "testrig")
// Mark some slots as in use
pool.MarkInUse("polecat-05")
pool.MarkInUse("polecat-10")
pool.MarkInUse("dementus")
pool.MarkInUse("valkyrie")
// Allocate should skip those
name, _ := pool.Allocate()
if name != "polecat-01" {
t.Errorf("expected polecat-01, got %s", name)
if name != "furiosa" {
t.Errorf("expected furiosa, got %s", name)
}
// Mark more and verify count
if pool.ActiveCount() != 3 { // 01, 05, 10
// Verify count
if pool.ActiveCount() != 3 { // furiosa, dementus, valkyrie
t.Errorf("expected 3 active, got %d", pool.ActiveCount())
}
}
@@ -313,3 +313,120 @@ func TestNamePool_StateFilePath(t *testing.T) {
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)
}
}