feat(namepool): auto-select theme per rig based on name hash
Each rig now gets a deterministic theme based on its name instead of always defaulting to mad-max. Uses a prime multiplier hash (×31) for good distribution across themes. Same rig name always gets the same theme. Users can still override with `gt namepool set`. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
fbc67e89e1
commit
74050cd0ab
@@ -529,8 +529,9 @@ func TestReconcilePoolWith(t *testing.T) {
|
|||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
// Create rig and manager (nil tmux for unit test)
|
// Create rig and manager (nil tmux for unit test)
|
||||||
|
// Use "myrig" which hashes to mad-max theme
|
||||||
r := &rig.Rig{
|
r := &rig.Rig{
|
||||||
Name: "testrig",
|
Name: "myrig",
|
||||||
Path: tmpDir,
|
Path: tmpDir,
|
||||||
}
|
}
|
||||||
m := NewManager(r, nil, nil)
|
m := NewManager(r, nil, nil)
|
||||||
@@ -591,8 +592,9 @@ func TestReconcilePoolWith_Allocation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
|
// Use "myrig" which hashes to mad-max theme
|
||||||
r := &rig.Rig{
|
r := &rig.Rig{
|
||||||
Name: "testrig",
|
Name: "myrig",
|
||||||
Path: tmpDir,
|
Path: tmpDir,
|
||||||
}
|
}
|
||||||
m := NewManager(r, nil, nil)
|
m := NewManager(r, nil, nil)
|
||||||
@@ -627,8 +629,9 @@ func TestReconcilePoolWith_OrphanDoesNotBlockAllocation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
|
// Use "myrig" which hashes to mad-max theme
|
||||||
r := &rig.Rig{
|
r := &rig.Rig{
|
||||||
Name: "testrig",
|
Name: "myrig",
|
||||||
Path: tmpDir,
|
Path: tmpDir,
|
||||||
}
|
}
|
||||||
m := NewManager(r, nil, nil)
|
m := NewManager(r, nil, nil)
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ type NamePool struct {
|
|||||||
func NewNamePool(rigPath, rigName string) *NamePool {
|
func NewNamePool(rigPath, rigName string) *NamePool {
|
||||||
return &NamePool{
|
return &NamePool{
|
||||||
RigName: rigName,
|
RigName: rigName,
|
||||||
Theme: DefaultTheme,
|
Theme: ThemeForRig(rigName),
|
||||||
InUse: make(map[string]bool),
|
InUse: make(map[string]bool),
|
||||||
OverflowNext: DefaultPoolSize + 1,
|
OverflowNext: DefaultPoolSize + 1,
|
||||||
MaxSize: DefaultPoolSize,
|
MaxSize: DefaultPoolSize,
|
||||||
@@ -352,6 +352,21 @@ func ListThemes() []string {
|
|||||||
return themes
|
return themes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ThemeForRig returns a deterministic theme for a rig based on its name.
|
||||||
|
// This provides variety across rigs without requiring manual configuration.
|
||||||
|
func ThemeForRig(rigName string) string {
|
||||||
|
themes := ListThemes()
|
||||||
|
if len(themes) == 0 {
|
||||||
|
return DefaultTheme
|
||||||
|
}
|
||||||
|
// Hash using prime multiplier for better distribution
|
||||||
|
var hash uint32
|
||||||
|
for _, b := range []byte(rigName) {
|
||||||
|
hash = hash*31 + uint32(b)
|
||||||
|
}
|
||||||
|
return themes[hash%uint32(len(themes))]
|
||||||
|
}
|
||||||
|
|
||||||
// GetThemeNames returns the names in a specific theme.
|
// GetThemeNames returns the names in a specific theme.
|
||||||
func GetThemeNames(theme string) ([]string, error) {
|
func GetThemeNames(theme string) ([]string, error) {
|
||||||
if names, ok := BuiltinThemes[theme]; ok {
|
if names, ok := BuiltinThemes[theme]; ok {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func TestNamePool_Allocate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
|
||||||
|
|
||||||
// First allocation should be first themed name (furiosa)
|
// First allocation should be first themed name (furiosa)
|
||||||
name, err := pool.Allocate()
|
name, err := pool.Allocate()
|
||||||
@@ -41,7 +41,7 @@ func TestNamePool_Release(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
|
||||||
|
|
||||||
// Allocate first two
|
// Allocate first two
|
||||||
name1, _ := pool.Allocate()
|
name1, _ := pool.Allocate()
|
||||||
@@ -68,7 +68,7 @@ func TestNamePool_PrefersOrder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
|
||||||
|
|
||||||
// Allocate first 5
|
// Allocate first 5
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
@@ -209,7 +209,7 @@ func TestNamePool_Reconcile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
|
||||||
|
|
||||||
// Simulate existing polecats from filesystem
|
// Simulate existing polecats from filesystem
|
||||||
existing := []string{"slit", "valkyrie", "some-other-name"}
|
existing := []string{"slit", "valkyrie", "some-other-name"}
|
||||||
@@ -234,7 +234,7 @@ func TestNamePool_IsPoolName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -263,7 +263,7 @@ func TestNamePool_ActiveNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
|
||||||
|
|
||||||
pool.Allocate() // furiosa
|
pool.Allocate() // furiosa
|
||||||
pool.Allocate() // nux
|
pool.Allocate() // nux
|
||||||
@@ -287,7 +287,7 @@ func TestNamePool_MarkInUse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
|
||||||
|
|
||||||
// Mark some slots as in use
|
// Mark some slots as in use
|
||||||
pool.MarkInUse("dementus")
|
pool.MarkInUse("dementus")
|
||||||
@@ -417,7 +417,7 @@ func TestNamePool_Reset(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "mad-max", nil, DefaultPoolSize)
|
||||||
|
|
||||||
// Allocate several names
|
// Allocate several names
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
@@ -441,3 +441,24 @@ func TestNamePool_Reset(t *testing.T) {
|
|||||||
t.Errorf("expected furiosa after reset, got %s", name)
|
t.Errorf("expected furiosa after reset, got %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestThemeForRig(t *testing.T) {
|
||||||
|
// Different rigs should get different themes (with high probability)
|
||||||
|
themes := make(map[string]bool)
|
||||||
|
for _, rigName := range []string{"gastown", "beads", "myproject", "webapp"} {
|
||||||
|
themes[ThemeForRig(rigName)] = true
|
||||||
|
}
|
||||||
|
// Should have at least 2 different themes across 4 rigs
|
||||||
|
if len(themes) < 2 {
|
||||||
|
t.Errorf("expected variety in themes, got only %d unique theme(s)", len(themes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThemeForRigDeterministic(t *testing.T) {
|
||||||
|
// Same rig name should always get same theme
|
||||||
|
theme1 := ThemeForRig("myrig")
|
||||||
|
theme2 := ThemeForRig("myrig")
|
||||||
|
if theme1 != theme2 {
|
||||||
|
t.Errorf("theme not deterministic: got %q and %q", theme1, theme2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user