1package cmd
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "os"
8 "testing"
9 "time"
10
11 "github.com/bnema/dumber/internal/application/usecase"
12 "github.com/bnema/dumber/internal/cli"
13 "github.com/bnema/dumber/internal/cli/styles"
14 "github.com/bnema/dumber/internal/domain/entity"
15 "github.com/bnema/dumber/internal/infrastructure/config"
16 "github.com/stretchr/testify/require"
17)
18
19func TestSessionCommandsReturnManagementErrorWhenSessionUseCaseMissing(t *testing.T) {
20 tests := []struct {
21 name string
22 run func() error
23 }{
24 {
25 name: "interactive sessions",
26 run: func() error { return runSessions(nil, nil) },
27 },
28 {
29 name: "list sessions",
30 run: func() error { return runSessionsList(nil, nil) },
31 },
32 {
33 name: "delete session",
34 run: func() error { return runSessionsDelete(nil, []string{"session-id"}) },
35 },
36 {
37 name: "find session helper",
38 run: func() error {
39 _, err := findSessionByIDOrSuffix("session-id")
40 return err
41 },
42 },
43 }
44
45 for _, tt := range tests {
46 t.Run(tt.name, func(t *testing.T) {
47 withApp(t, sessionCommandTestApp())
48 discardStderr(t)
49
50 err := tt.run()
51
52 require.ErrorContains(t, err, "session management not available")
53 })
54 }
55}
56
57func TestSessionRestoreReturnsRestorationErrorWhenSessionUseCaseMissing(t *testing.T) {
58 withApp(t, sessionCommandTestApp())
59 discardStderr(t)
60
61 err := runSessionsRestore(nil, []string{"session-id"})
62
63 require.ErrorContains(t, err, "session restoration not available")
64}
65
66func TestFindSessionByIDOrSuffixSearchesBeyondDefaultLimit(t *testing.T) {
67 targetID := entity.SessionID("20251224_000000_targetolder")
68 sessions := make([]*entity.Session, 0, defaultSessionsLimit*5+1)
69 startedAt := time.Date(2025, 12, 24, 12, 0, 0, 0, time.UTC)
70 for i := range defaultSessionsLimit * 5 {
71 sessions = append(sessions, &entity.Session{
72 ID: entity.SessionID(fmt.Sprintf("20251224_1200%02d_recent", i)),
73 Type: entity.SessionTypeBrowser,
74 StartedAt: startedAt.Add(-time.Duration(i) * time.Minute),
75 })
76 }
77 sessions = append(sessions, &entity.Session{
78 ID: targetID,
79 Type: entity.SessionTypeBrowser,
80 StartedAt: startedAt.Add(-time.Hour),
81 })
82
83 sessionRepo := &sessionCommandSessionRepo{sessions: sessions}
84 cfg := config.DefaultConfig()
85 withApp(t, &cli.App{
86 Config: cfg,
87 Theme: styles.NewTheme(cfg),
88 SessionUC: usecase.NewManageSessionUseCase(sessionRepo, nil),
89 ListSessionsUC: usecase.NewListSessionsUseCase(sessionRepo, sessionCommandStateRepo{}),
90 })
91
92 got, err := findSessionByIDOrSuffix("targetolder")
93
94 require.NoError(t, err)
95 require.Equal(t, targetID, got.Session.ID)
96}
97
98func sessionCommandTestApp() *cli.App {
99 cfg := config.DefaultConfig()
100 return &cli.App{
101 Config: cfg,
102 Theme: styles.NewTheme(cfg),
103 ListSessionsUC: &usecase.ListSessionsUseCase{},
104 RestoreUC: &usecase.RestoreSessionUseCase{},
105 DeleteSessionUC: &usecase.DeleteSessionUseCase{},
106 }
107}
108
109func withApp(t *testing.T, testApp *cli.App) {
110 t.Helper()
111 oldApp := app
112 app = testApp
113 t.Cleanup(func() { app = oldApp })
114}
115
116func discardStderr(t *testing.T) {
117 t.Helper()
118 oldStderr := os.Stderr
119 reader, writer, err := os.Pipe()
120 require.NoError(t, err)
121 os.Stderr = writer
122 t.Cleanup(func() {
123 os.Stderr = oldStderr
124 require.NoError(t, writer.Close())
125 _, _ = io.Copy(io.Discard, reader)
126 require.NoError(t, reader.Close())
127 })
128}
129
130type sessionCommandSessionRepo struct {
131 sessions []*entity.Session
132}
133
134func (*sessionCommandSessionRepo) Save(context.Context, *entity.Session) error {
135 return nil
136}
137
138func (r *sessionCommandSessionRepo) FindByID(_ context.Context, id entity.SessionID) (*entity.Session, error) {
139 for _, session := range r.sessions {
140 if session.ID == id {
141 return session, nil
142 }
143 }
144 return nil, nil
145}
146
147func (*sessionCommandSessionRepo) GetActive(context.Context) (*entity.Session, error) {
148 return nil, nil
149}
150
151func (r *sessionCommandSessionRepo) GetRecent(_ context.Context, limit int) ([]*entity.Session, error) {
152 if limit < 0 || limit > len(r.sessions) {
153 limit = len(r.sessions)
154 }
155 out := make([]*entity.Session, limit)
156 copy(out, r.sessions[:limit])
157 return out, nil
158}
159
160func (*sessionCommandSessionRepo) MarkEnded(context.Context, entity.SessionID, time.Time) error {
161 return nil
162}
163
164func (*sessionCommandSessionRepo) Delete(context.Context, entity.SessionID) error {
165 return nil
166}
167
168func (*sessionCommandSessionRepo) DeleteOldestExited(context.Context, int) (int64, error) {
169 return 0, nil
170}
171
172func (*sessionCommandSessionRepo) DeleteExitedBefore(context.Context, time.Time) (int64, error) {
173 return 0, nil
174}
175
176type sessionCommandStateRepo struct{}
177
178func (sessionCommandStateRepo) SaveSnapshot(context.Context, *entity.SessionState) error {
179 return nil
180}
181
182func (sessionCommandStateRepo) GetSnapshot(context.Context, entity.SessionID) (*entity.SessionState, error) {
183 return nil, nil
184}
185
186func (sessionCommandStateRepo) DeleteSnapshot(context.Context, entity.SessionID) error {
187 return nil
188}
189
190func (sessionCommandStateRepo) GetAllSnapshots(context.Context) ([]*entity.SessionState, error) {
191 return nil, nil
192}
193
194func (sessionCommandStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) {
195 return 0, nil
196}