refactor: Remove in-memory swarms map, make Manager stateless

The swarm Manager was maintaining an in-memory map of swarms that was
never persisted and duplicated state from beads. This caused stale state
after restarts and confusion about source of truth.

Changes:
- Remove swarms map from Manager (now stateless)
- Add LoadSwarm() that queries beads for swarm state
- Refactor all methods to use LoadSwarm() instead of in-memory lookup
- Discover workers from assigned tasks in beads
- Remove obsolete unit tests that tested in-memory behavior
- Keep type/state tests that do not need beads

The E2E test (gt-kc7yj.4) now covers the beads integration.

🤖 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-29 17:12:18 -08:00
parent 5fa1772cb0
commit f7393b6cdb
6 changed files with 147 additions and 460 deletions

View File

@@ -6,182 +6,23 @@ import (
"github.com/steveyegge/gastown/internal/rig"
)
func TestManagerCreate(t *testing.T) {
func TestNewManager(t *testing.T) {
r := &rig.Rig{
Name: "test-rig",
Path: "/tmp/test-rig",
}
m := NewManager(r)
swarm, err := m.Create("epic-1", []string{"Toast", "Nux"}, "main")
if err != nil {
t.Fatalf("Create failed: %v", err)
if m == nil {
t.Fatal("NewManager returned nil")
}
if swarm.ID != "epic-1" {
t.Errorf("ID = %q, want %q", swarm.ID, "epic-1")
if m.rig != r {
t.Error("Manager rig not set correctly")
}
if swarm.State != SwarmCreated {
t.Errorf("State = %q, want %q", swarm.State, SwarmCreated)
}
if len(swarm.Workers) != 2 {
t.Errorf("Workers = %d, want 2", len(swarm.Workers))
if m.workDir != r.Path {
t.Errorf("workDir = %q, want %q", m.workDir, r.Path)
}
}
func TestManagerCreateDuplicate(t *testing.T) {
r := &rig.Rig{
Name: "test-rig",
Path: "/tmp/test-rig",
}
m := NewManager(r)
_, err := m.Create("epic-1", []string{"Toast"}, "main")
if err != nil {
t.Fatalf("First Create failed: %v", err)
}
_, err = m.Create("epic-1", []string{"Nux"}, "main")
if err != ErrSwarmExists {
t.Errorf("Create duplicate = %v, want ErrSwarmExists", err)
}
}
func TestManagerStateTransitions(t *testing.T) {
r := &rig.Rig{
Name: "test-rig",
Path: "/tmp/test-rig",
}
m := NewManager(r)
swarm, _ := m.Create("epic-1", []string{"Toast"}, "main")
// Start
if err := m.Start(swarm.ID); err != nil {
t.Errorf("Start failed: %v", err)
}
s, _ := m.GetSwarm(swarm.ID)
if s.State != SwarmActive {
t.Errorf("State after Start = %q, want %q", s.State, SwarmActive)
}
// Can't start again
if err := m.Start(swarm.ID); err == nil {
t.Error("Start from Active should fail")
}
// Transition to Merging
if err := m.UpdateState(swarm.ID, SwarmMerging); err != nil {
t.Errorf("UpdateState to Merging failed: %v", err)
}
// Transition to Landed
if err := m.UpdateState(swarm.ID, SwarmLanded); err != nil {
t.Errorf("UpdateState to Landed failed: %v", err)
}
// Can't transition from terminal
if err := m.UpdateState(swarm.ID, SwarmActive); err == nil {
t.Error("UpdateState from Landed should fail")
}
}
func TestManagerCancel(t *testing.T) {
r := &rig.Rig{
Name: "test-rig",
Path: "/tmp/test-rig",
}
m := NewManager(r)
swarm, _ := m.Create("epic-1", []string{"Toast"}, "main")
_ = m.Start(swarm.ID)
if err := m.Cancel(swarm.ID, "user requested"); err != nil {
t.Errorf("Cancel failed: %v", err)
}
s, _ := m.GetSwarm(swarm.ID)
if s.State != SwarmCancelled {
t.Errorf("State after Cancel = %q, want %q", s.State, SwarmCancelled)
}
if s.Error != "user requested" {
t.Errorf("Error = %q, want %q", s.Error, "user requested")
}
}
func TestManagerTaskOperations(t *testing.T) {
r := &rig.Rig{
Name: "test-rig",
Path: "/tmp/test-rig",
}
m := NewManager(r)
swarm, _ := m.Create("epic-1", []string{"Toast"}, "main")
// Manually add tasks (normally loaded from beads)
swarm.Tasks = []SwarmTask{
{IssueID: "task-1", Title: "Task 1", State: TaskPending},
{IssueID: "task-2", Title: "Task 2", State: TaskPending},
}
// Get ready tasks
ready, err := m.GetReadyTasks(swarm.ID)
if err != nil {
t.Errorf("GetReadyTasks failed: %v", err)
}
if len(ready) != 2 {
t.Errorf("GetReadyTasks = %d, want 2", len(ready))
}
// Assign task
if err := m.AssignTask(swarm.ID, "task-1", "Toast"); err != nil {
t.Errorf("AssignTask failed: %v", err)
}
// Check assignment
s, _ := m.GetSwarm(swarm.ID)
if s.Tasks[0].Assignee != "Toast" {
t.Errorf("Assignee = %q, want %q", s.Tasks[0].Assignee, "Toast")
}
if s.Tasks[0].State != TaskAssigned {
t.Errorf("State = %q, want %q", s.Tasks[0].State, TaskAssigned)
}
// Update state
if err := m.UpdateTaskState(swarm.ID, "task-1", TaskMerged); err != nil {
t.Errorf("UpdateTaskState failed: %v", err)
}
s, _ = m.GetSwarm(swarm.ID)
if s.Tasks[0].State != TaskMerged {
t.Errorf("State = %q, want %q", s.Tasks[0].State, TaskMerged)
}
if s.Tasks[0].MergedAt == nil {
t.Error("MergedAt should be set")
}
}
func TestManagerIsComplete(t *testing.T) {
r := &rig.Rig{
Name: "test-rig",
Path: "/tmp/test-rig",
}
m := NewManager(r)
swarm, _ := m.Create("epic-1", []string{"Toast"}, "main")
swarm.Tasks = []SwarmTask{
{IssueID: "task-1", State: TaskPending},
{IssueID: "task-2", State: TaskMerged},
}
complete, _ := m.IsComplete(swarm.ID)
if complete {
t.Error("IsComplete should be false with pending task")
}
// Complete the pending task
_ = m.UpdateTaskState(swarm.ID, "task-1", TaskMerged)
complete, _ = m.IsComplete(swarm.ID)
if !complete {
t.Error("IsComplete should be true when all tasks merged")
}
}
// Note: Most swarm tests require integration with beads.
// See gt-kc7yj.4 for the E2E integration test.