package switcher import ( "testing" "time" "forge.secuaas.ovh/olivier/claude-failover/internal/config" "forge.secuaas.ovh/olivier/claude-failover/internal/quota" "forge.secuaas.ovh/olivier/claude-failover/internal/state" ) // mockTmux for switcher tests. type mockTmux struct { sessions map[string]bool paneOutput map[string]string killCalls []string createCalls []string } func newMockTmux() *mockTmux { return &mockTmux{ sessions: make(map[string]bool), paneOutput: make(map[string]string), } } func (m *mockTmux) HasSession(name string) bool { return m.sessions[name] } func (m *mockTmux) CreateSession(name, _ 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(_, _ string) error { return nil } func (m *mockTmux) CapturePaneTail(session string, _ int) (string, error) { return m.paneOutput[session], nil } // TestFindTargetAccount returns the first account that differs from current. func TestFindTargetAccount(t *testing.T) { tc := newMockTmux() s := state.New("") cfg := &config.Config{ Accounts: []config.AccountConfig{ {Name: "compte1", Priority: 1}, {Name: "compte2", Priority: 2}, }, } a := New(tc, s, cfg, make(chan quota.SwitchRequest), nil) target := a.findTargetAccount("compte1") if target == nil || target.Name != "compte2" { t.Errorf("expected compte2, got %v", target) } } // TestFindTargetAccountSingleAccount returns nil when only one account exists. func TestFindTargetAccountSingleAccount(t *testing.T) { tc := newMockTmux() s := state.New("") cfg := &config.Config{ Accounts: []config.AccountConfig{{Name: "solo"}}, } a := New(tc, s, cfg, make(chan quota.SwitchRequest), nil) if got := a.findTargetAccount("solo"); got != nil { t.Errorf("expected nil for single account, got %v", got) } } // TestExtractResumeUUID parses UUID from pane output. func TestExtractResumeUUID(t *testing.T) { input := "$ claude --resume a1b2c3d4-e5f6-7890-abcd-ef1234567890 --model sonnet" got := extractResumeUUID(input) want := "a1b2c3d4-e5f6-7890-abcd-ef1234567890" if got != want { t.Errorf("expected %q, got %q", want, got) } } // TestExtractResumeUUIDMissing returns empty string when no UUID present. func TestExtractResumeUUIDMissing(t *testing.T) { if got := extractResumeUUID("no uuid here"); got != "" { t.Errorf("expected empty, got %q", got) } } // TestTimeUntilReset parses minute and hour formats correctly. func TestTimeUntilReset(t *testing.T) { cases := []struct { input string want time.Duration }{ {"in 45 minutes", 45 * time.Minute}, {"in 2 hours", 2 * time.Hour}, {"in 1 hour", 1 * time.Hour}, {"", 2 * time.Hour}, {"8pm", 2 * time.Hour}, // fallback for unrecognised formats } for _, c := range cases { if got := timeUntilReset(c.input); got != c.want { t.Errorf("timeUntilReset(%q) = %v, want %v", c.input, got, c.want) } } } // TestKillAndRecreatePoolSessions verifies that executeSwitch restarts sessions. func TestKillAndRecreatePoolSessions(t *testing.T) { tc := newMockTmux() tc.sessions["ccl-auto-0"] = true tc.sessions["ccl-auto-1"] = true tc.sessions["dedicated-1"] = true s := state.New("") s.SetActiveAccount("compte1") cfg := &config.Config{ Accounts: []config.AccountConfig{ {Name: "compte1", Home: t.TempDir()}, {Name: "compte2", Home: t.TempDir()}, }, Pool: config.PoolConfig{ Dedicated: []config.DedicatedSession{{Name: "dedicated-1", Project: "/tmp"}}, Autonomous: config.AutonomousConfig{Prefix: "ccl-auto-", Min: 2, Max: 2}, }, } a := New(tc, s, cfg, make(chan quota.SwitchRequest), nil) // CRITICAL: isolate symlink manipulation in a tmpdir so the test never // touches the real ~/.claude (regression: a reboot used to leave Claude // Code unusable because the test had repointed ~/.claude to /tmp/...). a.homeDir = t.TempDir() a.executeSwitch(quota.SwitchRequest{From: "compte1"}) // Active account must have changed. if got := s.ActiveAccount(); got != "compte2" { t.Errorf("expected active account compte2, got %q", got) } // All old sessions must have been killed. for _, name := range []string{"ccl-auto-0", "ccl-auto-1", "dedicated-1"} { found := false for _, k := range tc.killCalls { if k == name { found = true break } } if !found { t.Errorf("expected %q to be killed", name) } } // Min pool sessions must be recreated. recreated := map[string]bool{} for _, c := range tc.createCalls { recreated[c] = true } if !recreated["ccl-auto-0"] || !recreated["ccl-auto-1"] { t.Errorf("expected autonomous sessions recreated; createCalls=%v", tc.createCalls) } }