package lifecycle import ( "testing" "forge.secuaas.ovh/olivier/claude-failover/internal/config" "forge.secuaas.ovh/olivier/claude-failover/internal/state" ) // mockTmux is a minimal in-memory implementation of tmux.Client for tests. type mockTmux struct { sessions map[string]bool createCalls []string killCalls []string sendKeysCalls []string } func newMockTmux() *mockTmux { return &mockTmux{sessions: make(map[string]bool)} } func (m *mockTmux) HasSession(name string) bool { return m.sessions[name] } func (m *mockTmux) CreateSession(name, workdir string) error { m.sessions[name] = true m.createCalls = append(m.createCalls, name) return nil } func (m *mockTmux) KillSession(name string) error { delete(m.sessions, name) m.killCalls = append(m.killCalls, name) return nil } func (m *mockTmux) SendKeys(session, keys string) error { m.sendKeysCalls = append(m.sendKeysCalls, session) return nil } func (m *mockTmux) SendEnter(session string) error { m.sendKeysCalls = append(m.sendKeysCalls, session) return nil } func (m *mockTmux) CapturePaneTail(session string, lines int) (string, error) { return "", nil } // minimalConfig returns a config with one dedicated session and no autonomous pool. func minimalConfig(sessionName, project string) *config.Config { return &config.Config{ Pool: config.PoolConfig{ Dedicated: []config.DedicatedSession{ {Name: sessionName, Project: project}, }, Autonomous: config.AutonomousConfig{ Prefix: "ccl-auto-", Min: 0, Max: 0, }, }, } } // TestReconcileCreatesDeadSession verifies that when a session is absent and // its state is idle, reconcile recreates it. func TestReconcileCreatesDeadSession(t *testing.T) { tc := newMockTmux() // Session does NOT exist in tmux. tc.sessions["my-session"] = false s := state.New("/tmp/test-state-idle.json") s.SetIdle("my-session") cfg := minimalConfig("my-session", "/tmp/project") m := New(tc, s, cfg) m.reconcile() if len(tc.createCalls) != 1 || tc.createCalls[0] != "my-session" { t.Errorf("expected CreateSession(my-session) to be called once; createCalls=%v", tc.createCalls) } if got := s.GetSession("my-session"); got == nil || got.State != "idle" { t.Errorf("expected session state idle after recreate, got %v", got) } } // TestReconcileRecoversCrashedSession verifies that a missing session whose // state is "working" gets marked failed before being recreated. func TestReconcileRecoversCrashedSession(t *testing.T) { tc := newMockTmux() // Session does NOT exist in tmux but was working. tc.sessions["worker"] = false s := state.New("/tmp/test-state-working.json") s.SetWorking("worker", "task-abc") cfg := minimalConfig("worker", "") m := New(tc, s, cfg) m.reconcile() if len(tc.createCalls) != 1 || tc.createCalls[0] != "worker" { t.Errorf("expected CreateSession(worker) once after recovery; createCalls=%v", tc.createCalls) } // State should transition: working -> failed -> idle (SetIdle called after recreate). got := s.GetSession("worker") if got == nil { t.Fatal("session state is nil after recovery") } if got.State != "idle" { t.Errorf("expected session state idle after recovery, got %q", got.State) } if got.LastFail == nil { t.Error("expected LastFail to be set after SetFailed was called") } } // TestEnsureAllSessions verifies that EnsureAllSessions creates all sessions // that are missing from tmux. func TestEnsureAllSessions(t *testing.T) { tc := newMockTmux() // None of the sessions exist yet. s := state.New("/tmp/test-state-ensure.json") cfg := &config.Config{ Pool: config.PoolConfig{ Dedicated: []config.DedicatedSession{ {Name: "sess-a", Project: "/tmp/a"}, {Name: "sess-b", Project: "/tmp/b"}, }, Autonomous: config.AutonomousConfig{ Prefix: "auto-", Min: 2, Max: 5, }, }, } m := New(tc, s, cfg) m.EnsureAllSessions() // Expect: sess-a, sess-b, auto-0, auto-1 = 4 sessions created. if len(tc.createCalls) != 4 { t.Errorf("expected 4 CreateSession calls, got %d: %v", len(tc.createCalls), tc.createCalls) } want := map[string]bool{"sess-a": true, "sess-b": true, "auto-0": true, "auto-1": true} for _, name := range tc.createCalls { if !want[name] { t.Errorf("unexpected session created: %q", name) } } }