package tui import ( "context" "strings" "testing" "superwork-tui/internal/cc" "superwork-tui/internal/store" ) func TestManagedSessions_OpenSetsState(t *testing.T) { m := New() m2, cmd := m.openManagedSessions() if m2.state != stateManagedSessions { t.Errorf("state = %v, want stateManagedSessions", m2.state) } if cmd == nil { t.Error("openManagedSessions should return a load cmd") } } func TestManagedSessions_EscReturnsToBoard(t *testing.T) { m := New() m.state = stateManagedSessions result, _ := m.handleManagedSessionsKey(keyMsg("esc")) m2 := result.(Model) if m2.state != stateLoaded { t.Errorf("expected stateLoaded, got %v", m2.state) } } func TestManagedSessions_CreateFiresLaunchCmd(t *testing.T) { origWatcher := watchForNewSessionFn watchForNewSessionFn = func(_ context.Context, _ cc.SessionWatchOpts) (string, error) { return "fake-session-id-123", nil } defer func() { watchForNewSessionFn = origWatcher }() tmpDir := t.TempDir() restore := overrideWorkspaceRoot(tmpDir) defer restore() rec := &recorder{ responses: map[string][]byte{ "has-session": {}, "list-windows": []byte(""), }, } sm := NewSessionManager(rec.run) m := New() m.state = stateManagedSessions m.sessionMgr = sm m.managedSessions = store.ManagedSessionsData{Sessions: []store.ManagedSession{}} result, _ := m.handleManagedSessionsKey(keyMsg("a")) m2 := result.(Model) if !m2.managedSessionsInputActive { t.Fatal("expected input active after 'a'") } if m2.managedSessionsInputMode != sessionsModeCreate { t.Errorf("mode = %v, want sessionsModeCreate", m2.managedSessionsInputMode) } m2.managedSessionsInput.SetValue("cap-sess") result2, launchCmd := m2.handleManagedSessionsKey(keyMsg("enter")) m3 := result2.(Model) if m3.managedSessionsInputActive { t.Error("input should be inactive after enter") } if len(m3.managedSessions.Sessions) != 0 { t.Errorf("sessions should be 0 before capture, got %d", len(m3.managedSessions.Sessions)) } if launchCmd == nil { t.Fatal("expected launch cmd") } captureMsg := launchCmd() captured, ok := captureMsg.(managedSessionCapturedMsg) if !ok { t.Fatalf("expected managedSessionCapturedMsg, got %T", captureMsg) } if captured.sessionID != "fake-session-id-123" { t.Errorf("captured.sessionID = %q, want fake-session-id-123", captured.sessionID) } if captured.name != "cap-sess" { t.Errorf("captured.name = %q, want cap-sess", captured.name) } result3, saveCmd := m3.Update(captureMsg) m4 := result3.(Model) if len(m4.managedSessions.Sessions) != 1 { t.Fatalf("sessions = %d, want 1 after capture", len(m4.managedSessions.Sessions)) } sess := m4.managedSessions.Sessions[0] if sess.ID != "fake-session-id-123" { t.Errorf("session ID = %q, want fake-session-id-123", sess.ID) } if sess.Name != "cap-sess" { t.Errorf("session Name = %q, want cap-sess", sess.Name) } if saveCmd == nil { t.Fatal("expected save cmd after capture") } } func TestManagedSessions_CreateTimeout_Drops(t *testing.T) { origWatcher := watchForNewSessionFn watchForNewSessionFn = func(_ context.Context, _ cc.SessionWatchOpts) (string, error) { return "", nil } defer func() { watchForNewSessionFn = origWatcher }() tmpDir := t.TempDir() restore := overrideWorkspaceRoot(tmpDir) defer restore() rec := &recorder{ responses: map[string][]byte{ "has-session": {}, "list-windows": []byte(""), }, } sm := NewSessionManager(rec.run) m := New() m.state = stateManagedSessions m.sessionMgr = sm m.managedSessions = store.ManagedSessionsData{Sessions: []store.ManagedSession{}} result, _ := m.handleManagedSessionsKey(keyMsg("a")) m2 := result.(Model) m2.managedSessionsInput.SetValue("timeout-sess") _, launchCmd := m2.handleManagedSessionsKey(keyMsg("enter")) if launchCmd == nil { t.Fatal("expected launch cmd") } timeoutMsg := launchCmd() captured, ok := timeoutMsg.(managedSessionCapturedMsg) if !ok { t.Fatalf("expected managedSessionCapturedMsg, got %T", timeoutMsg) } if captured.sessionID != "" { t.Errorf("expected empty sessionID on timeout, got %q", captured.sessionID) } result2, _ := m.Update(timeoutMsg) m3 := result2.(Model) if len(m3.managedSessions.Sessions) != 0 { t.Errorf("sessions should be 0 after timeout, got %d", len(m3.managedSessions.Sessions)) } } func TestManagedSessions_CapturedID_ResumeBuildsCommand(t *testing.T) { tmpDir := t.TempDir() restore := overrideWorkspaceRoot(tmpDir) defer restore() rec := &recorder{ responses: map[string][]byte{ "has-session": {}, "list-windows": []byte(""), }, } sm := NewSessionManager(rec.run) m := New() m.state = stateManagedSessions m.sessionMgr = sm m.managedSessions = store.ManagedSessionsData{ Sessions: []store.ManagedSession{ {ID: "captured-abc123", Name: "mysess", CreatedAt: 100}, }, } m.managedSessionsCursor = 0 _, resumeCmd := m.handleManagedSessionsKey(keyMsg("enter")) if resumeCmd == nil { t.Fatal("expected resume cmd") } msg := resumeCmd() result, ok := msg.(sessionActionResultMsg) if !ok { t.Fatalf("expected sessionActionResultMsg, got %T", msg) } if result.err != nil { t.Errorf("unexpected error: %v", result.err) } var newWinCmd string for _, call := range rec.calls { if len(call) > 0 && call[0] == "new-window" { newWinCmd = call[len(call)-1] } } if !strings.Contains(newWinCmd, "--resume") || !strings.Contains(newWinCmd, "captured-abc123") { t.Errorf("new-window command missing --resume captured-abc123, got %q", newWinCmd) } } func TestManagedSessions_DeleteConfirm_ShowsName(t *testing.T) { m := New() m.state = stateManagedSessions m.managedSessions = store.ManagedSessionsData{ Sessions: []store.ManagedSession{ {ID: "uuid-123", Name: "my-named-session", CreatedAt: 1}, }, } m.managedSessionsCursor = 0 m.managedSessionsDeleteID = "uuid-123" out := m.managedSessionsView() if !strings.Contains(out, "my-named-session") { t.Errorf("delete confirm should show session name, got %q", out) } if strings.Contains(out, "确认删除会话 \"uuid-123\"") { t.Error("delete confirm should not show raw ID") } } func TestManagedSessions_RenameSession(t *testing.T) { tmpDir := t.TempDir() restore := overrideWorkspaceRoot(tmpDir) defer restore() m := New() m.state = stateManagedSessions m.managedSessions = store.ManagedSessionsData{ Sessions: []store.ManagedSession{ {ID: "abc123", Name: "old-name", CreatedAt: 100}, }, } m.managedSessionsCursor = 0 // Press 'r' result, _ := m.handleManagedSessionsKey(keyMsg("r")) m2 := result.(Model) if !m2.managedSessionsInputActive { t.Fatal("expected input active after 'r'") } if m2.managedSessionsInputMode != sessionsModeRename { t.Errorf("mode = %v, want sessionsModeRename", m2.managedSessionsInputMode) } m2.managedSessionsInput.SetValue("new-name") result2, _ := m2.handleManagedSessionsKey(keyMsg("enter")) m3 := result2.(Model) if m3.managedSessions.Sessions[0].Name != "new-name" { t.Errorf("name = %q, want new-name", m3.managedSessions.Sessions[0].Name) } } func TestManagedSessions_DeleteSession(t *testing.T) { tmpDir := t.TempDir() restore := overrideWorkspaceRoot(tmpDir) defer restore() m := New() m.state = stateManagedSessions m.managedSessions = store.ManagedSessionsData{ Sessions: []store.ManagedSession{ {ID: "abc123", Name: "to-delete", CreatedAt: 100}, {ID: "def456", Name: "keep", CreatedAt: 200}, }, } m.managedSessionsCursor = 0 // Press 'd' result, _ := m.handleManagedSessionsKey(keyMsg("d")) m2 := result.(Model) if m2.managedSessionsDeleteID != "abc123" { t.Errorf("deleteID = %q, want abc123", m2.managedSessionsDeleteID) } // Confirm 'y' result2, saveCmd := m2.handleManagedSessionsKey(keyMsg("y")) m3 := result2.(Model) if m3.managedSessionsDeleteID != "" { t.Error("deleteID should be cleared after confirmation") } if len(m3.managedSessions.Sessions) != 1 { t.Errorf("sessions = %d, want 1", len(m3.managedSessions.Sessions)) } if m3.managedSessions.Sessions[0].ID != "def456" { t.Errorf("remaining session ID = %q, want def456", m3.managedSessions.Sessions[0].ID) } if saveCmd != nil { msg := saveCmd() if sv, ok := msg.(managedSessionsSavedMsg); ok && sv.err != nil { t.Errorf("save error: %v", sv.err) } } } func TestManagedSessions_DeleteConfirmN_Cancels(t *testing.T) { m := New() m.state = stateManagedSessions m.managedSessions = store.ManagedSessionsData{ Sessions: []store.ManagedSession{ {ID: "abc", Name: "x", CreatedAt: 1}, }, } m.managedSessionsCursor = 0 m.managedSessionsDeleteID = "abc" result, _ := m.handleManagedSessionsKey(keyMsg("n")) m2 := result.(Model) if m2.managedSessionsDeleteID != "" { t.Error("deleteID should be cleared on 'n'") } if len(m2.managedSessions.Sessions) != 1 { t.Error("sessions should not be deleted on 'n'") } } func TestManagedSessions_ResumeEmptyID_Error(t *testing.T) { rec := &recorder{ responses: map[string][]byte{ "has-session": {}, }, } sm := NewSessionManager(rec.run) m := New() m.state = stateManagedSessions m.sessionMgr = sm m.managedSessions = store.ManagedSessionsData{ Sessions: []store.ManagedSession{ {ID: "", Name: "no-id", CreatedAt: 100}, }, } m.managedSessionsCursor = 0 _, resumeCmd := m.handleManagedSessionsKey(keyMsg("enter")) if resumeCmd == nil { t.Fatal("expected resume cmd") } msg := resumeCmd() result, ok := msg.(sessionActionResultMsg) if !ok { t.Fatalf("expected sessionActionResultMsg, got %T", msg) } if result.err == nil { t.Error("expected error for empty ID resume") } } func TestManagedSessions_ResumeWithID_OpensWindow(t *testing.T) { tmpDir := t.TempDir() restore := overrideWorkspaceRoot(tmpDir) defer restore() rec := &recorder{ responses: map[string][]byte{ "has-session": {}, "list-windows": []byte(""), }, } sm := NewSessionManager(rec.run) m := New() m.state = stateManagedSessions m.sessionMgr = sm m.managedSessions = store.ManagedSessionsData{ Sessions: []store.ManagedSession{ {ID: "sess-abc123", Name: "my-sess", ProfilePath: "/p/dev.json", CreatedAt: 100}, }, } m.managedSessionsCursor = 0 _, resumeCmd := m.handleManagedSessionsKey(keyMsg("enter")) if resumeCmd == nil { t.Fatal("expected resume cmd") } msg := resumeCmd() result, ok := msg.(sessionActionResultMsg) if !ok { t.Fatalf("expected sessionActionResultMsg, got %T", msg) } if result.err != nil { t.Errorf("unexpected error: %v", result.err) } // Check tmux received new-window with the resume command var newWinCmd string for _, call := range rec.calls { if len(call) > 0 && call[0] == "new-window" { newWinCmd = call[len(call)-1] } } if !strings.Contains(newWinCmd, "--resume") || !strings.Contains(newWinCmd, "sess-abc123") { t.Errorf("new-window command missing resume args, got %q", newWinCmd) } } func TestManagedSessionsView_ContainsSessionInfo(t *testing.T) { m := New() m.state = stateManagedSessions m.managedSessions = store.ManagedSessionsData{ Sessions: []store.ManagedSession{ {ID: "abc123", Name: "my-session", ProfilePath: "/dev.json", CreatedAt: 1000}, }, } m.managedSessionsCursor = 0 out := m.managedSessionsView() for _, want := range []string{"my-session", "abc123"} { if !strings.Contains(out, want) { t.Errorf("managedSessionsView missing %q", want) } } }