Files
gastown/internal/polecat/manager_test.go
Steve Yegge 231d6e92e0 feat: implement ephemeral polecat model
This implements the ephemeral polecat model where polecats are spawned
fresh for each task and deleted upon completion.

Key changes:

**Spawn (internal/cmd/spawn.go):**
- Always create fresh worktree from main branch
- Run bd init in new worktree to initialize beads
- Remove --create flag (now implicit)
- Replace stale polecats with fresh worktrees

**Handoff (internal/cmd/handoff.go):**
- Add rig/polecat detection from environment and tmux session
- Send shutdown requests to correct witness (rig/witness)
- Include polecat name in lifecycle request body

**Witness (internal/witness/manager.go):**
- Add mail checking in monitoring loop
- Process LIFECYCLE shutdown requests
- Implement full cleanup sequence:
  - Kill tmux session
  - Remove git worktree
  - Delete polecat branch

**Polecat state machine (internal/polecat/types.go):**
- Primary states: working, done, stuck
- Deprecate idle/active (kept for backward compatibility)
- New polecats start in working state
- ClearIssue transitions to done (not idle)

**Polecat commands (internal/cmd/polecat.go):**
- Update list to show "Active Polecats"
- Normalize legacy states for display
- Add deprecation warnings to wake/sleep commands

Closes gt-7ik

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

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

315 lines
6.6 KiB
Go

package polecat
import (
"os"
"path/filepath"
"testing"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/rig"
)
func TestStateIsActive(t *testing.T) {
tests := []struct {
state State
active bool
}{
{StateWorking, true},
{StateDone, false},
{StateStuck, false},
// Legacy states are treated as active
{StateIdle, true},
{StateActive, true},
}
for _, tt := range tests {
if got := tt.state.IsActive(); got != tt.active {
t.Errorf("%s.IsActive() = %v, want %v", tt.state, got, tt.active)
}
}
}
func TestStateIsWorking(t *testing.T) {
tests := []struct {
state State
working bool
}{
{StateIdle, false},
{StateActive, false},
{StateWorking, true},
{StateDone, false},
{StateStuck, false},
}
for _, tt := range tests {
if got := tt.state.IsWorking(); got != tt.working {
t.Errorf("%s.IsWorking() = %v, want %v", tt.state, got, tt.working)
}
}
}
func TestPolecatSummary(t *testing.T) {
p := &Polecat{
Name: "Toast",
State: StateWorking,
Issue: "gt-abc",
}
summary := p.Summary()
if summary.Name != "Toast" {
t.Errorf("Name = %q, want Toast", summary.Name)
}
if summary.State != StateWorking {
t.Errorf("State = %v, want StateWorking", summary.State)
}
if summary.Issue != "gt-abc" {
t.Errorf("Issue = %q, want gt-abc", summary.Issue)
}
}
func TestListEmpty(t *testing.T) {
root := t.TempDir()
r := &rig.Rig{
Name: "test-rig",
Path: root,
}
m := NewManager(r, git.NewGit(root))
polecats, err := m.List()
if err != nil {
t.Fatalf("List: %v", err)
}
if len(polecats) != 0 {
t.Errorf("polecats count = %d, want 0", len(polecats))
}
}
func TestGetNotFound(t *testing.T) {
root := t.TempDir()
r := &rig.Rig{
Name: "test-rig",
Path: root,
}
m := NewManager(r, git.NewGit(root))
_, err := m.Get("nonexistent")
if err != ErrPolecatNotFound {
t.Errorf("Get = %v, want ErrPolecatNotFound", err)
}
}
func TestRemoveNotFound(t *testing.T) {
root := t.TempDir()
r := &rig.Rig{
Name: "test-rig",
Path: root,
}
m := NewManager(r, git.NewGit(root))
err := m.Remove("nonexistent", false)
if err != ErrPolecatNotFound {
t.Errorf("Remove = %v, want ErrPolecatNotFound", err)
}
}
func TestPolecatDir(t *testing.T) {
r := &rig.Rig{
Name: "test-rig",
Path: "/home/user/ai/test-rig",
}
m := NewManager(r, git.NewGit(r.Path))
dir := m.polecatDir("Toast")
expected := "/home/user/ai/test-rig/polecats/Toast"
if dir != expected {
t.Errorf("polecatDir = %q, want %q", dir, expected)
}
}
func TestStateFile(t *testing.T) {
r := &rig.Rig{
Name: "test-rig",
Path: "/home/user/ai/test-rig",
}
m := NewManager(r, git.NewGit(r.Path))
file := m.stateFile("Toast")
expected := "/home/user/ai/test-rig/polecats/Toast/state.json"
if file != expected {
t.Errorf("stateFile = %q, want %q", file, expected)
}
}
func TestStatePersistence(t *testing.T) {
root := t.TempDir()
polecatDir := filepath.Join(root, "polecats", "Test")
if err := os.MkdirAll(polecatDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: root,
}
m := NewManager(r, git.NewGit(root))
// Save state
polecat := &Polecat{
Name: "Test",
Rig: "test-rig",
State: StateWorking,
ClonePath: polecatDir,
Issue: "gt-xyz",
}
if err := m.saveState(polecat); err != nil {
t.Fatalf("saveState: %v", err)
}
// Load state
loaded, err := m.loadState("Test")
if err != nil {
t.Fatalf("loadState: %v", err)
}
if loaded.Name != "Test" {
t.Errorf("Name = %q, want Test", loaded.Name)
}
if loaded.State != StateWorking {
t.Errorf("State = %v, want StateWorking", loaded.State)
}
if loaded.Issue != "gt-xyz" {
t.Errorf("Issue = %q, want gt-xyz", loaded.Issue)
}
}
func TestListWithPolecats(t *testing.T) {
root := t.TempDir()
// Create some polecat directories with state files
for _, name := range []string{"Toast", "Cheedo"} {
polecatDir := filepath.Join(root, "polecats", name)
if err := os.MkdirAll(polecatDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
}
r := &rig.Rig{
Name: "test-rig",
Path: root,
}
m := NewManager(r, git.NewGit(root))
polecats, err := m.List()
if err != nil {
t.Fatalf("List: %v", err)
}
if len(polecats) != 2 {
t.Errorf("polecats count = %d, want 2", len(polecats))
}
}
func TestSetState(t *testing.T) {
root := t.TempDir()
polecatDir := filepath.Join(root, "polecats", "Test")
if err := os.MkdirAll(polecatDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: root,
}
m := NewManager(r, git.NewGit(root))
// Initial state
if err := m.saveState(&Polecat{Name: "Test", State: StateIdle}); err != nil {
t.Fatalf("saveState: %v", err)
}
// Update state
if err := m.SetState("Test", StateActive); err != nil {
t.Fatalf("SetState: %v", err)
}
// Verify
polecat, err := m.Get("Test")
if err != nil {
t.Fatalf("Get: %v", err)
}
if polecat.State != StateActive {
t.Errorf("State = %v, want StateActive", polecat.State)
}
}
func TestAssignIssue(t *testing.T) {
root := t.TempDir()
polecatDir := filepath.Join(root, "polecats", "Test")
if err := os.MkdirAll(polecatDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: root,
}
m := NewManager(r, git.NewGit(root))
// Initial state
if err := m.saveState(&Polecat{Name: "Test", State: StateIdle}); err != nil {
t.Fatalf("saveState: %v", err)
}
// Assign issue
if err := m.AssignIssue("Test", "gt-abc"); err != nil {
t.Fatalf("AssignIssue: %v", err)
}
// Verify
polecat, err := m.Get("Test")
if err != nil {
t.Fatalf("Get: %v", err)
}
if polecat.Issue != "gt-abc" {
t.Errorf("Issue = %q, want gt-abc", polecat.Issue)
}
if polecat.State != StateWorking {
t.Errorf("State = %v, want StateWorking", polecat.State)
}
}
func TestClearIssue(t *testing.T) {
root := t.TempDir()
polecatDir := filepath.Join(root, "polecats", "Test")
if err := os.MkdirAll(polecatDir, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: root,
}
m := NewManager(r, git.NewGit(root))
// Initial state with issue
if err := m.saveState(&Polecat{Name: "Test", State: StateWorking, Issue: "gt-abc"}); err != nil {
t.Fatalf("saveState: %v", err)
}
// Clear issue
if err := m.ClearIssue("Test"); err != nil {
t.Fatalf("ClearIssue: %v", err)
}
// Verify - in ephemeral model, ClearIssue transitions to Done
polecat, err := m.Get("Test")
if err != nil {
t.Fatalf("Get: %v", err)
}
if polecat.Issue != "" {
t.Errorf("Issue = %q, want empty", polecat.Issue)
}
if polecat.State != StateDone {
t.Errorf("State = %v, want StateDone", polecat.State)
}
}