get_last_restorable_session_test.go RawHistoryFolder
1package usecase_test
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7 "time"
8
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/domain/entity"
11 repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14)
15
16func TestGetLastRestorableSessionUseCase_NoSessions(t *testing.T) {
17 ctx := testContext()
18
19 sessionRepo := repomocks.NewMockSessionRepository(t)
20 stateRepo := repomocks.NewMockSessionStateRepository(t)
21
22 sessionRepo.EXPECT().GetRecent(ctx, 10).Return([]*entity.Session{}, nil)
23
24 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, "")
25
26 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
27 ExcludeSessionID: "current",
28 })
29
30 require.NoError(t, err)
31 require.NotNil(t, output)
32 assert.Empty(t, output.SessionID)
33 assert.Nil(t, output.State)
34}
35
36func TestGetLastRestorableSessionUseCase_OnlyCurrentSession(t *testing.T) {
37 ctx := testContext()
38
39 sessionRepo := repomocks.NewMockSessionRepository(t)
40 stateRepo := repomocks.NewMockSessionStateRepository(t)
41
42 currentID := entity.SessionID("20251225_120000_curr")
43 endedAt := time.Now()
44
45 sessions := []*entity.Session{
46 {
47 ID: currentID,
48 Type: entity.SessionTypeBrowser,
49 StartedAt: time.Now().Add(-1 * time.Hour),
50 EndedAt: &endedAt,
51 },
52 }
53
54 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
55
56 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, "")
57
58 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
59 ExcludeSessionID: currentID,
60 })
61
62 require.NoError(t, err)
63 require.NotNil(t, output)
64 assert.Empty(t, output.SessionID)
65}
66
67func TestGetLastRestorableSessionUseCase_SkipsCLISessions(t *testing.T) {
68 ctx := testContext()
69
70 sessionRepo := repomocks.NewMockSessionRepository(t)
71 stateRepo := repomocks.NewMockSessionStateRepository(t)
72
73 endedAt := time.Now()
74 sessions := []*entity.Session{
75 {
76 ID: "20251225_120000_cli1",
77 Type: entity.SessionTypeCLI, // Not a browser session
78 StartedAt: time.Now().Add(-1 * time.Hour),
79 EndedAt: &endedAt,
80 },
81 }
82
83 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
84
85 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, "")
86
87 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
88 ExcludeSessionID: "current",
89 })
90
91 require.NoError(t, err)
92 assert.Empty(t, output.SessionID)
93}
94
95func TestGetLastRestorableSessionUseCase_SkipsActiveWithLock(t *testing.T) {
96 ctx := testContext()
97
98 // Create temp dir for lock files
99 lockDir := t.TempDir()
100
101 sessionRepo := repomocks.NewMockSessionRepository(t)
102 stateRepo := repomocks.NewMockSessionStateRepository(t)
103
104 activeID := entity.SessionID("20251225_120000_actv")
105
106 // Create lock file to simulate running session
107 lockPath := filepath.Join(lockDir, "session_"+string(activeID)+".lock")
108 f, err := os.Create(lockPath)
109 require.NoError(t, err)
110 f.Close()
111
112 sessions := []*entity.Session{
113 {
114 ID: activeID,
115 Type: entity.SessionTypeBrowser,
116 StartedAt: time.Now().Add(-1 * time.Hour),
117 EndedAt: nil, // Active (no ended_at)
118 },
119 }
120
121 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
122
123 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, lockDir)
124
125 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
126 ExcludeSessionID: "current",
127 })
128
129 require.NoError(t, err)
130 assert.Empty(t, output.SessionID)
131}
132
133func TestGetLastRestorableSessionUseCase_SkipsNoSnapshot(t *testing.T) {
134 ctx := testContext()
135
136 sessionRepo := repomocks.NewMockSessionRepository(t)
137 stateRepo := repomocks.NewMockSessionStateRepository(t)
138
139 endedAt := time.Now()
140 sessionID := entity.SessionID("20251225_120000_nosn")
141
142 sessions := []*entity.Session{
143 {
144 ID: sessionID,
145 Type: entity.SessionTypeBrowser,
146 StartedAt: time.Now().Add(-1 * time.Hour),
147 EndedAt: &endedAt,
148 },
149 }
150
151 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
152 stateRepo.EXPECT().GetSnapshot(ctx, sessionID).Return(nil, nil) // No snapshot
153
154 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, "")
155
156 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
157 ExcludeSessionID: "current",
158 })
159
160 require.NoError(t, err)
161 assert.Empty(t, output.SessionID)
162}
163
164func TestGetLastRestorableSessionUseCase_SkipsEmptyTabs(t *testing.T) {
165 ctx := testContext()
166
167 sessionRepo := repomocks.NewMockSessionRepository(t)
168 stateRepo := repomocks.NewMockSessionStateRepository(t)
169
170 endedAt := time.Now()
171 sessionID := entity.SessionID("20251225_120000_emty")
172
173 sessions := []*entity.Session{
174 {
175 ID: sessionID,
176 Type: entity.SessionTypeBrowser,
177 StartedAt: time.Now().Add(-1 * time.Hour),
178 EndedAt: &endedAt,
179 },
180 }
181
182 emptyState := &entity.SessionState{
183 Version: entity.SessionStateVersion,
184 SessionID: sessionID,
185 Tabs: []entity.TabSnapshot{}, // No tabs
186 SavedAt: time.Now(),
187 }
188
189 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
190 stateRepo.EXPECT().GetSnapshot(ctx, sessionID).Return(emptyState, nil)
191
192 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, "")
193
194 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
195 ExcludeSessionID: "current",
196 })
197
198 require.NoError(t, err)
199 assert.Empty(t, output.SessionID)
200}
201
202func TestGetLastRestorableSessionUseCase_ReturnsEndedSession(t *testing.T) {
203 ctx := testContext()
204
205 sessionRepo := repomocks.NewMockSessionRepository(t)
206 stateRepo := repomocks.NewMockSessionStateRepository(t)
207
208 endedAt := time.Now()
209 sessionID := entity.SessionID("20251225_120000_good")
210
211 sessions := []*entity.Session{
212 {
213 ID: sessionID,
214 Type: entity.SessionTypeBrowser,
215 StartedAt: time.Now().Add(-1 * time.Hour),
216 EndedAt: &endedAt, // Gracefully ended
217 },
218 }
219
220 validState := &entity.SessionState{
221 Version: entity.SessionStateVersion,
222 SessionID: sessionID,
223 Tabs: []entity.TabSnapshot{
224 {
225 ID: "tab-1",
226 Name: "Test Tab",
227 Workspace: entity.WorkspaceSnapshot{
228 Root: &entity.PaneNodeSnapshot{
229 Pane: &entity.PaneSnapshot{
230 ID: "pane-1",
231 URI: "https://example.com",
232 Title: "Example",
233 },
234 },
235 },
236 },
237 },
238 SavedAt: time.Now(),
239 }
240
241 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
242 stateRepo.EXPECT().GetSnapshot(ctx, sessionID).Return(validState, nil)
243
244 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, "")
245
246 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
247 ExcludeSessionID: "current",
248 })
249
250 require.NoError(t, err)
251 require.NotNil(t, output)
252 assert.Equal(t, sessionID, output.SessionID)
253 assert.Equal(t, validState, output.State)
254}
255
256func TestGetLastRestorableSessionUseCase_ReturnsCrashedSession(t *testing.T) {
257 ctx := testContext()
258
259 // Empty lock dir (no lock files)
260 lockDir := t.TempDir()
261
262 sessionRepo := repomocks.NewMockSessionRepository(t)
263 stateRepo := repomocks.NewMockSessionStateRepository(t)
264
265 sessionID := entity.SessionID("20251225_120000_crsh")
266
267 // Session with no ended_at (crashed) but no lock file
268 sessions := []*entity.Session{
269 {
270 ID: sessionID,
271 Type: entity.SessionTypeBrowser,
272 StartedAt: time.Now().Add(-1 * time.Hour),
273 EndedAt: nil, // Not ended - simulates crash
274 },
275 }
276
277 validState := &entity.SessionState{
278 Version: entity.SessionStateVersion,
279 SessionID: sessionID,
280 Tabs: []entity.TabSnapshot{
281 {
282 ID: "tab-1",
283 Name: "Crashed Tab",
284 Workspace: entity.WorkspaceSnapshot{
285 Root: &entity.PaneNodeSnapshot{
286 Pane: &entity.PaneSnapshot{
287 ID: "pane-1",
288 URI: "https://example.com",
289 Title: "Example",
290 },
291 },
292 },
293 },
294 },
295 SavedAt: time.Now(),
296 }
297
298 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
299 stateRepo.EXPECT().GetSnapshot(ctx, sessionID).Return(validState, nil)
300
301 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, lockDir)
302
303 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
304 ExcludeSessionID: "current",
305 })
306
307 require.NoError(t, err)
308 require.NotNil(t, output)
309 assert.Equal(t, sessionID, output.SessionID)
310 assert.Equal(t, validState, output.State)
311}
312
313func TestGetLastRestorableSessionUseCase_ReturnsMostRecent(t *testing.T) {
314 ctx := testContext()
315
316 sessionRepo := repomocks.NewMockSessionRepository(t)
317 stateRepo := repomocks.NewMockSessionStateRepository(t)
318
319 endedAt := time.Now()
320 recentID := entity.SessionID("20251225_140000_new1")
321 olderID := entity.SessionID("20251225_120000_old1")
322
323 // Sessions ordered by started_at DESC (most recent first)
324 sessions := []*entity.Session{
325 {
326 ID: recentID,
327 Type: entity.SessionTypeBrowser,
328 StartedAt: time.Now().Add(-1 * time.Hour),
329 EndedAt: &endedAt,
330 },
331 {
332 ID: olderID,
333 Type: entity.SessionTypeBrowser,
334 StartedAt: time.Now().Add(-2 * time.Hour),
335 EndedAt: &endedAt,
336 },
337 }
338
339 recentState := &entity.SessionState{
340 Version: entity.SessionStateVersion,
341 SessionID: recentID,
342 Tabs: []entity.TabSnapshot{
343 {ID: "tab-1", Name: "Recent Tab"},
344 },
345 SavedAt: time.Now(),
346 }
347
348 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
349 stateRepo.EXPECT().GetSnapshot(ctx, recentID).Return(recentState, nil)
350 // Note: GetSnapshot for olderID should NOT be called since we return first match
351
352 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, "")
353
354 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
355 ExcludeSessionID: "current",
356 })
357
358 require.NoError(t, err)
359 assert.Equal(t, recentID, output.SessionID)
360}
361
362func TestGetLastRestorableSessionUseCase_SkipsFirstIfNoSnapshot(t *testing.T) {
363 ctx := testContext()
364
365 sessionRepo := repomocks.NewMockSessionRepository(t)
366 stateRepo := repomocks.NewMockSessionStateRepository(t)
367
368 endedAt := time.Now()
369 noSnapID := entity.SessionID("20251225_140000_nosn")
370 goodID := entity.SessionID("20251225_120000_good")
371
372 sessions := []*entity.Session{
373 {
374 ID: noSnapID,
375 Type: entity.SessionTypeBrowser,
376 StartedAt: time.Now().Add(-1 * time.Hour),
377 EndedAt: &endedAt,
378 },
379 {
380 ID: goodID,
381 Type: entity.SessionTypeBrowser,
382 StartedAt: time.Now().Add(-2 * time.Hour),
383 EndedAt: &endedAt,
384 },
385 }
386
387 goodState := &entity.SessionState{
388 Version: entity.SessionStateVersion,
389 SessionID: goodID,
390 Tabs: []entity.TabSnapshot{
391 {ID: "tab-1", Name: "Good Tab"},
392 },
393 SavedAt: time.Now(),
394 }
395
396 sessionRepo.EXPECT().GetRecent(ctx, 10).Return(sessions, nil)
397 stateRepo.EXPECT().GetSnapshot(ctx, noSnapID).Return(nil, nil) // No snapshot
398 stateRepo.EXPECT().GetSnapshot(ctx, goodID).Return(goodState, nil)
399
400 uc := usecase.NewGetLastRestorableSessionUseCase(sessionRepo, stateRepo, "")
401
402 output, err := uc.Execute(ctx, usecase.GetLastRestorableSessionInput{
403 ExcludeSessionID: "current",
404 })
405
406 require.NoError(t, err)
407 assert.Equal(t, goodID, output.SessionID)
408}