refactor: unify agent startup with Manager pattern
- Create mayor.Manager for mayor lifecycle (Start/Stop/IsRunning/Status) - Create deacon.Manager for deacon lifecycle with respawn loop - Move session.Manager to polecat.SessionManager (clearer naming) - Add zombie session detection for mayor/deacon (kills tmux if Claude dead) - Remove duplicate session startup code from up.go, start.go, mayor.go - Rename sessMgr -> polecatMgr for consistency - Make witness/refinery SessionName() public for status display All agent types now follow the same Manager pattern: mgr := agent.NewManager(...) mgr.Start(...) mgr.Stop() mgr.IsRunning() mgr.Status() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
192
internal/polecat/session_manager_test.go
Normal file
192
internal/polecat/session_manager_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package polecat
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
|
||||
func TestSessionName(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "gastown",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
name := m.SessionName("Toast")
|
||||
if name != "gt-gastown-Toast" {
|
||||
t.Errorf("sessionName = %q, want gt-gastown-Toast", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionManagerPolecatDir(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "gastown",
|
||||
Path: "/home/user/ai/gastown",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
dir := m.polecatDir("Toast")
|
||||
expected := "/home/user/ai/gastown/polecats/Toast"
|
||||
if dir != expected {
|
||||
t.Errorf("polecatDir = %q, want %q", dir, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasPolecat(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
// hasPolecat checks filesystem, so create actual directories
|
||||
for _, name := range []string{"Toast", "Cheedo"} {
|
||||
if err := os.MkdirAll(filepath.Join(root, "polecats", name), 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
r := &rig.Rig{
|
||||
Name: "gastown",
|
||||
Path: root,
|
||||
Polecats: []string{"Toast", "Cheedo"},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
if !m.hasPolecat("Toast") {
|
||||
t.Error("expected hasPolecat(Toast) = true")
|
||||
}
|
||||
if !m.hasPolecat("Cheedo") {
|
||||
t.Error("expected hasPolecat(Cheedo) = true")
|
||||
}
|
||||
if m.hasPolecat("Unknown") {
|
||||
t.Error("expected hasPolecat(Unknown) = false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPolecatNotFound(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "gastown",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
err := m.Start("Unknown", SessionStartOptions{})
|
||||
if err == nil {
|
||||
t.Error("expected error for unknown polecat")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRunningNoSession(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "gastown",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
running, err := m.IsRunning("Toast")
|
||||
if err != nil {
|
||||
t.Fatalf("IsRunning: %v", err)
|
||||
}
|
||||
if running {
|
||||
t.Error("expected IsRunning = false for non-existent session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionManagerListEmpty(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig-unlikely-name",
|
||||
Polecats: []string{},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
infos, err := m.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(infos) != 0 {
|
||||
t.Errorf("infos count = %d, want 0", len(infos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopNotFound(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
err := m.Stop("Toast", false)
|
||||
if err != ErrSessionNotFound {
|
||||
t.Errorf("Stop = %v, want ErrSessionNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureNotFound(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
_, err := m.Capture("Toast", 50)
|
||||
if err != ErrSessionNotFound {
|
||||
t.Errorf("Capture = %v, want ErrSessionNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectNotFound(t *testing.T) {
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig",
|
||||
Polecats: []string{"Toast"},
|
||||
}
|
||||
m := NewSessionManager(tmux.NewTmux(), r)
|
||||
|
||||
err := m.Inject("Toast", "hello")
|
||||
if err != ErrSessionNotFound {
|
||||
t.Errorf("Inject = %v, want ErrSessionNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPolecatCommandFormat verifies the polecat session command exports
|
||||
// GT_ROLE, GT_RIG, GT_POLECAT, and BD_ACTOR inline before starting Claude.
|
||||
// This is a regression test for gt-y41ep - env vars must be exported inline
|
||||
// because tmux SetEnvironment only affects new panes, not the current shell.
|
||||
func TestPolecatCommandFormat(t *testing.T) {
|
||||
// This test verifies the expected command format.
|
||||
// The actual command is built in Start() but we test the format here
|
||||
// to document and verify the expected behavior.
|
||||
|
||||
rigName := "gastown"
|
||||
polecatName := "Toast"
|
||||
expectedBdActor := "gastown/polecats/Toast"
|
||||
|
||||
// Build the expected command format (mirrors Start() logic)
|
||||
expectedPrefix := "export GT_ROLE=polecat GT_RIG=" + rigName + " GT_POLECAT=" + polecatName + " BD_ACTOR=" + expectedBdActor + " GIT_AUTHOR_NAME=" + expectedBdActor
|
||||
expectedSuffix := "&& claude --dangerously-skip-permissions"
|
||||
|
||||
// The command must contain all required env exports
|
||||
requiredParts := []string{
|
||||
"export",
|
||||
"GT_ROLE=polecat",
|
||||
"GT_RIG=" + rigName,
|
||||
"GT_POLECAT=" + polecatName,
|
||||
"BD_ACTOR=" + expectedBdActor,
|
||||
"GIT_AUTHOR_NAME=" + expectedBdActor,
|
||||
"claude --dangerously-skip-permissions",
|
||||
}
|
||||
|
||||
// Verify expected format contains all required parts
|
||||
fullCommand := expectedPrefix + " " + expectedSuffix
|
||||
for _, part := range requiredParts {
|
||||
if !strings.Contains(fullCommand, part) {
|
||||
t.Errorf("Polecat command should contain %q", part)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify GT_ROLE is specifically "polecat" (not "mayor" or "crew")
|
||||
if !strings.Contains(fullCommand, "GT_ROLE=polecat") {
|
||||
t.Error("GT_ROLE must be 'polecat', not 'mayor' or 'crew'")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user