package polecat import ( "os" "os/exec" "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 active state is treated as active {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 }{ {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 TestAssigneeID(t *testing.T) { r := &rig.Rig{ Name: "test-rig", Path: "/home/user/ai/test-rig", } m := NewManager(r, git.NewGit(r.Path)) id := m.assigneeID("Toast") expected := "test-rig/Toast" if id != expected { t.Errorf("assigneeID = %q, want %q", id, expected) } } // Note: State persistence tests removed - state is now derived from beads assignee field. // Integration tests should verify beads-based state management. func TestGetReturnsWorkingWithoutBeads(t *testing.T) { // When beads is not available, Get should return StateWorking // (assume the polecat is doing something if it exists) // // Skip if bd is installed - the test assumes bd is unavailable, but when bd // is present it queries beads and returns actual state instead of defaulting. if _, err := exec.LookPath("bd"); err == nil { t.Skip("skipping: bd is installed, test requires bd to be unavailable") } root := t.TempDir() polecatDir := filepath.Join(root, "polecats", "Test") if err := os.MkdirAll(polecatDir, 0755); err != nil { t.Fatalf("mkdir: %v", err) } // Create mayor/rig directory for beads (but no actual beads) mayorRigDir := filepath.Join(root, "mayor", "rig") if err := os.MkdirAll(mayorRigDir, 0755); err != nil { t.Fatalf("mkdir mayor/rig: %v", err) } r := &rig.Rig{ Name: "test-rig", Path: root, } m := NewManager(r, git.NewGit(root)) // Get should return polecat with StateWorking (assume active if beads unavailable) polecat, err := m.Get("Test") if err != nil { t.Fatalf("Get: %v", err) } if polecat.Name != "Test" { t.Errorf("Name = %q, want Test", polecat.Name) } if polecat.State != StateWorking { t.Errorf("State = %v, want StateWorking (beads not available)", polecat.State) } } func TestListWithPolecats(t *testing.T) { root := t.TempDir() // Create some polecat directories (state is now derived from beads, not 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) } } if err := os.MkdirAll(filepath.Join(root, "polecats", ".claude"), 0755); err != nil { t.Fatalf("mkdir .claude: %v", err) } // Create mayor/rig for beads path mayorRig := filepath.Join(root, "mayor", "rig") if err := os.MkdirAll(mayorRig, 0755); err != nil { t.Fatalf("mkdir mayor/rig: %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)) } } // Note: TestSetState, TestAssignIssue, and TestClearIssue were removed. // These operations now require a running beads instance and are tested // via integration tests. The unit tests here focus on testing the basic // polecat lifecycle operations that don't require beads. func TestSetStateWithoutBeads(t *testing.T) { // SetState should not error when beads is not available root := t.TempDir() polecatDir := filepath.Join(root, "polecats", "Test") if err := os.MkdirAll(polecatDir, 0755); err != nil { t.Fatalf("mkdir: %v", err) } // Create mayor/rig for beads path mayorRig := filepath.Join(root, "mayor", "rig") if err := os.MkdirAll(mayorRig, 0755); err != nil { t.Fatalf("mkdir mayor/rig: %v", err) } r := &rig.Rig{ Name: "test-rig", Path: root, } m := NewManager(r, git.NewGit(root)) // SetState should succeed (no-op when no issue assigned) err := m.SetState("Test", StateActive) if err != nil { t.Errorf("SetState: %v (expected no error when no beads/issue)", err) } } func TestClearIssueWithoutAssignment(t *testing.T) { // ClearIssue should not error when no issue is assigned root := t.TempDir() polecatDir := filepath.Join(root, "polecats", "Test") if err := os.MkdirAll(polecatDir, 0755); err != nil { t.Fatalf("mkdir: %v", err) } // Create mayor/rig for beads path mayorRig := filepath.Join(root, "mayor", "rig") if err := os.MkdirAll(mayorRig, 0755); err != nil { t.Fatalf("mkdir mayor/rig: %v", err) } r := &rig.Rig{ Name: "test-rig", Path: root, } m := NewManager(r, git.NewGit(root)) // ClearIssue should succeed even when no issue assigned err := m.ClearIssue("Test") if err != nil { t.Errorf("ClearIssue: %v (expected no error when no assignment)", err) } } // NOTE: TestInstallCLAUDETemplate tests were removed. // We no longer write CLAUDE.md to worktrees - Gas Town context is injected // ephemerally via SessionStart hook (gt prime) to prevent leaking internal // architecture into project repos. func TestAddWithOptions_HasAgentsMD(t *testing.T) { // This test verifies that AGENTS.md exists in polecat worktrees after creation. // AGENTS.md is critical for polecats to "land the plane" properly. root := t.TempDir() // Create mayor/rig directory structure (this acts as repo base when no .repo.git) mayorRig := filepath.Join(root, "mayor", "rig") if err := os.MkdirAll(mayorRig, 0755); err != nil { t.Fatalf("mkdir mayor/rig: %v", err) } // Initialize git repo in mayor/rig cmd := exec.Command("git", "init") cmd.Dir = mayorRig if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git init: %v\n%s", err, out) } // Create AGENTS.md with test content agentsMDContent := []byte("# AGENTS.md\n\nTest content for polecats.\n") agentsMDPath := filepath.Join(mayorRig, "AGENTS.md") if err := os.WriteFile(agentsMDPath, agentsMDContent, 0644); err != nil { t.Fatalf("write AGENTS.md: %v", err) } // Commit AGENTS.md so it's part of the repo mayorGit := git.NewGit(mayorRig) if err := mayorGit.Add("AGENTS.md"); err != nil { t.Fatalf("git add: %v", err) } if err := mayorGit.Commit("Add AGENTS.md"); err != nil { t.Fatalf("git commit: %v", err) } // AddWithOptions needs origin/main to exist. Add self as origin and fetch. cmd = exec.Command("git", "remote", "add", "origin", mayorRig) cmd.Dir = mayorRig if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git remote add: %v\n%s", err, out) } if err := mayorGit.Fetch("origin"); err != nil { t.Fatalf("git fetch: %v", err) } // Create rig pointing to root r := &rig.Rig{ Name: "rig", Path: root, } m := NewManager(r, git.NewGit(root)) // Create polecat via AddWithOptions polecat, err := m.AddWithOptions("TestAgent", AddOptions{}) if err != nil { t.Fatalf("AddWithOptions: %v", err) } // Verify AGENTS.md exists in the worktree worktreeAgentsMD := filepath.Join(polecat.ClonePath, "AGENTS.md") if _, err := os.Stat(worktreeAgentsMD); os.IsNotExist(err) { t.Errorf("AGENTS.md does not exist in worktree at %s", worktreeAgentsMD) } // Verify content matches content, err := os.ReadFile(worktreeAgentsMD) if err != nil { t.Fatalf("read worktree AGENTS.md: %v", err) } if string(content) != string(agentsMDContent) { t.Errorf("AGENTS.md content = %q, want %q", string(content), string(agentsMDContent)) } } func TestAddWithOptions_AgentsMDFallback(t *testing.T) { // This test verifies the fallback: if AGENTS.md is not in git, // it should be copied from mayor/rig. root := t.TempDir() // Create mayor/rig directory structure mayorRig := filepath.Join(root, "mayor", "rig") if err := os.MkdirAll(mayorRig, 0755); err != nil { t.Fatalf("mkdir mayor/rig: %v", err) } // Initialize git repo in mayor/rig WITHOUT AGENTS.md in git cmd := exec.Command("git", "init") cmd.Dir = mayorRig if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git init: %v\n%s", err, out) } // Create a dummy file and commit (repo needs at least one commit) dummyPath := filepath.Join(mayorRig, "README.md") if err := os.WriteFile(dummyPath, []byte("# Test\n"), 0644); err != nil { t.Fatalf("write README.md: %v", err) } mayorGit := git.NewGit(mayorRig) if err := mayorGit.Add("README.md"); err != nil { t.Fatalf("git add: %v", err) } if err := mayorGit.Commit("Initial commit"); err != nil { t.Fatalf("git commit: %v", err) } // AddWithOptions needs origin/main to exist. Add self as origin and fetch. cmd = exec.Command("git", "remote", "add", "origin", mayorRig) cmd.Dir = mayorRig if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git remote add: %v\n%s", err, out) } if err := mayorGit.Fetch("origin"); err != nil { t.Fatalf("git fetch: %v", err) } // Now create AGENTS.md in mayor/rig (but NOT committed to git) // This simulates the fallback scenario agentsMDContent := []byte("# AGENTS.md\n\nFallback content.\n") agentsMDPath := filepath.Join(mayorRig, "AGENTS.md") if err := os.WriteFile(agentsMDPath, agentsMDContent, 0644); err != nil { t.Fatalf("write AGENTS.md: %v", err) } // Create rig pointing to root r := &rig.Rig{ Name: "rig", Path: root, } m := NewManager(r, git.NewGit(root)) // Create polecat via AddWithOptions polecat, err := m.AddWithOptions("TestFallback", AddOptions{}) if err != nil { t.Fatalf("AddWithOptions: %v", err) } // Verify AGENTS.md exists in the worktree (via fallback copy) worktreeAgentsMD := filepath.Join(polecat.ClonePath, "AGENTS.md") if _, err := os.Stat(worktreeAgentsMD); os.IsNotExist(err) { t.Errorf("AGENTS.md does not exist in worktree (fallback failed) at %s", worktreeAgentsMD) } // Verify content matches the fallback source content, err := os.ReadFile(worktreeAgentsMD) if err != nil { t.Fatalf("read worktree AGENTS.md: %v", err) } if string(content) != string(agentsMDContent) { t.Errorf("AGENTS.md content = %q, want %q", string(content), string(agentsMDContent)) } }