detached 5041353

get_last_restorable_session_test.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close