v0.23.2 bfd5eb4

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}

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