v0.32.0 0617278

snapshot_session_test.go RawHistoryFolder

1package usecase_test
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	"github.com/bnema/dumber/internal/application/usecase"
9	"github.com/bnema/dumber/internal/domain/entity"
10	repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/mock"
13	"github.com/stretchr/testify/require"
14)
15
16func TestSnapshotSessionUseCase_Execute_SavesSnapshot(t *testing.T) {
17	ctx := testContext()
18
19	stateRepo := repomocks.NewMockSessionStateRepository(t)
20
21	// Create a tab list with some tabs
22	tabList := entity.NewTabList()
23	pane := entity.NewPane("pane-1")
24	pane.URI = "https://example.com"
25	pane.Title = "Example"
26	tab := entity.NewTab("tab-1", "ws-1", pane)
27	tab.Name = "Test Tab"
28	tabList.Add(tab)
29
30	sessionID := entity.SessionID("20251224_120000_test")
31
32	stateRepo.EXPECT().SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
33		Run(func(_ context.Context, state *entity.SessionState) {
34			require.Equal(t, sessionID, state.SessionID)
35			require.Len(t, state.Tabs, 1)
36			require.Equal(t, "Test Tab", state.Tabs[0].Name)
37			require.Equal(t, entity.LegacySessionStateVersion, state.Version)
38		}).
39		Return(nil)
40
41	uc := usecase.NewSnapshotSessionUseCase(stateRepo)
42
43	err := uc.Execute(ctx, usecase.SnapshotInput{
44		SessionID: sessionID,
45		TabList:   tabList,
46	})
47	require.NoError(t, err)
48}
49
50func TestSnapshotSessionUseCase_Execute_EmptyTabList(t *testing.T) {
51	ctx := testContext()
52
53	stateRepo := repomocks.NewMockSessionStateRepository(t)
54
55	sessionID := entity.SessionID("20251224_120000_empty")
56
57	stateRepo.EXPECT().SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
58		Run(func(_ context.Context, state *entity.SessionState) {
59			require.Equal(t, sessionID, state.SessionID)
60			require.Empty(t, state.Tabs)
61		}).
62		Return(nil)
63
64	uc := usecase.NewSnapshotSessionUseCase(stateRepo)
65
66	err := uc.Execute(ctx, usecase.SnapshotInput{
67		SessionID: sessionID,
68		TabList:   nil,
69	})
70	require.NoError(t, err)
71}
72
73func TestSnapshotSessionUseCase_Execute_RequiresSessionID(t *testing.T) {
74	ctx := testContext()
75
76	stateRepo := repomocks.NewMockSessionStateRepository(t)
77
78	uc := usecase.NewSnapshotSessionUseCase(stateRepo)
79
80	err := uc.Execute(ctx, usecase.SnapshotInput{
81		SessionID: "",
82		TabList:   entity.NewTabList(),
83	})
84	require.Error(t, err)
85	assert.Contains(t, err.Error(), "session id required")
86}
87
88func TestSnapshotSessionUseCase_Execute_RejectsAmbiguousWindowAndTabListInput(t *testing.T) {
89	ctx := testContext()
90
91	stateRepo := repomocks.NewMockSessionStateRepository(t)
92	uc := usecase.NewSnapshotSessionUseCase(stateRepo)
93
94	err := uc.Execute(ctx, usecase.SnapshotInput{
95		SessionID: entity.SessionID("20251224_120000_ambiguous"),
96		TabList:   entity.NewTabList(),
97		Windows:   []entity.WindowTabListState{},
98	})
99	require.Error(t, err)
100	assert.Contains(t, err.Error(), "windows and tab list are mutually exclusive")
101}
102
103func TestSnapshotSessionUseCase_Execute_SavesEmptyWindowSnapshot(t *testing.T) {
104	ctx := testContext()
105
106	stateRepo := repomocks.NewMockSessionStateRepository(t)
107	sessionID := entity.SessionID("20251224_120000_empty_v2_windows")
108
109	stateRepo.EXPECT().SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
110		Run(func(_ context.Context, state *entity.SessionState) {
111			require.Equal(t, sessionID, state.SessionID)
112			require.Equal(t, entity.SessionStateVersion, state.Version)
113			require.Empty(t, state.Windows)
114			require.Empty(t, state.Tabs)
115			assert.Equal(t, 0, state.ActiveWindowIndex)
116		}).
117		Return(nil)
118
119	uc := usecase.NewSnapshotSessionUseCase(stateRepo)
120
121	err := uc.Execute(ctx, usecase.SnapshotInput{
122		SessionID:         sessionID,
123		Windows:           []entity.WindowTabListState{},
124		ActiveWindowIndex: 7,
125	})
126	require.NoError(t, err)
127}
128
129func TestSnapshotFromTabList_CreatesCorrectSnapshot(t *testing.T) {
130	sessionID := entity.SessionID("20251224_120000_snap")
131
132	// Create a workspace with multiple panes
133	pane1 := entity.NewPane("pane-1")
134	pane1.URI = "https://github.com"
135	pane1.Title = "GitHub"
136	pane1.ZoomFactor = 1.2
137
138	pane2 := entity.NewPane("pane-2")
139	pane2.URI = "https://google.com"
140	pane2.Title = "Google"
141
142	tab := entity.NewTab("tab-1", "ws-1", pane1)
143	tab.Name = "Dev Tab"
144	tab.IsPinned = true
145
146	tabList := entity.NewTabList()
147	tabList.Add(tab)
148
149	state := entity.SnapshotFromTabList(sessionID, tabList)
150
151	require.NotNil(t, state)
152	assert.Equal(t, entity.LegacySessionStateVersion, state.Version)
153	assert.Equal(t, sessionID, state.SessionID)
154	assert.Len(t, state.Tabs, 1)
155	assert.Equal(t, "Dev Tab", state.Tabs[0].Name)
156	assert.True(t, state.Tabs[0].IsPinned)
157	assert.NotNil(t, state.Tabs[0].Workspace.Root)
158	assert.Equal(t, "https://github.com", state.Tabs[0].Workspace.Root.Pane.URI)
159	assert.InDelta(t, 1.2, state.Tabs[0].Workspace.Root.Pane.ZoomFactor, 0.001)
160	assert.False(t, state.SavedAt.IsZero())
161}
162
163func TestSnapshotSessionUseCase_Execute_SavesWindowSnapshots(t *testing.T) {
164	ctx := testContext()
165
166	stateRepo := repomocks.NewMockSessionStateRepository(t)
167
168	// Create two windows, each with tabs
169	pane1_1 := entity.NewPane("p1_w1")
170	pane1_1.URI = "https://window1.com"
171	tab1_1 := entity.NewTab("t1_w1", "ws1_w1", pane1_1)
172	tab1_1.Name = "Window1 Tab1"
173
174	pane1_2 := entity.NewPane("p1_w2")
175	pane1_2.URI = "https://window2.com"
176	tab1_2 := entity.NewTab("t1_w2", "ws1_w2", pane1_2)
177	tab1_2.Name = "Window2 Tab1"
178
179	tabs1 := entity.NewTabList()
180	tabs1.Add(tab1_1)
181	tabs2 := entity.NewTabList()
182	tabs2.Add(tab1_2)
183
184	sessionID := entity.SessionID("20251224_120000_v2_windows")
185
186	windows := []entity.WindowTabListState{
187		{WindowID: "w1", Tabs: tabs1},
188		{WindowID: "w2", Tabs: tabs2},
189	}
190
191	stateRepo.EXPECT().SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
192		Run(func(_ context.Context, state *entity.SessionState) {
193			require.Equal(t, sessionID, state.SessionID)
194			require.Equal(t, 2, state.Version)
195			// Windows field populated
196			require.Len(t, state.Windows, 2)
197			assert.Equal(t, entity.WindowID("w1"), state.Windows[0].ID)
198			assert.Equal(t, entity.WindowID("w2"), state.Windows[1].ID)
199			// Window 1 has 1 tab
200			require.Len(t, state.Windows[0].Tabs, 1)
201			assert.Equal(t, "Window1 Tab1", state.Windows[0].Tabs[0].Name)
202			// Window 2 has 1 tab
203			require.Len(t, state.Windows[1].Tabs, 1)
204			assert.Equal(t, "Window2 Tab1", state.Windows[1].Tabs[0].Name)
205			// Active window index set
206			assert.Equal(t, 0, state.ActiveWindowIndex)
207			// Legacy Tabs empty when windows are provided
208			assert.Empty(t, state.Tabs)
209		}).
210		Return(nil)
211
212	uc := usecase.NewSnapshotSessionUseCase(stateRepo)
213
214	err := uc.Execute(ctx, usecase.SnapshotInput{
215		SessionID:         sessionID,
216		Windows:           windows,
217		ActiveWindowIndex: 0,
218	})
219	require.NoError(t, err)
220}
221
222func TestSessionState_CountPanes(t *testing.T) {
223	state := &entity.SessionState{
224		Tabs: []entity.TabSnapshot{
225			{
226				Workspace: entity.WorkspaceSnapshot{
227					Root: &entity.PaneNodeSnapshot{
228						Pane: &entity.PaneSnapshot{ID: "pane-1"},
229					},
230				},
231			},
232			{
233				Workspace: entity.WorkspaceSnapshot{
234					Root: &entity.PaneNodeSnapshot{
235						Children: []*entity.PaneNodeSnapshot{
236							{Pane: &entity.PaneSnapshot{ID: "pane-2"}},
237							{Pane: &entity.PaneSnapshot{ID: "pane-3"}},
238						},
239					},
240				},
241			},
242		},
243	}
244
245	assert.Equal(t, 3, state.CountPanes())
246}
247
248func TestGetRelativeTime(t *testing.T) {
249	now := time.Now()
250
251	tests := []struct {
252		name     string
253		t        time.Time
254		expected string
255	}{
256		{"just now", now.Add(-30 * time.Second), "just now"},
257		{"1 minute", now.Add(-1 * time.Minute), "1m ago"},
258		{"5 minutes", now.Add(-5 * time.Minute), "05m ago"},
259		{"1 hour", now.Add(-1 * time.Hour), "1h ago"},
260		{"3 hours", now.Add(-3 * time.Hour), "03h ago"},
261		{"1 day", now.Add(-24 * time.Hour), "1d ago"},
262		{"2 days", now.Add(-48 * time.Hour), "02d ago"},
263		{"1 week", now.Add(-7 * 24 * time.Hour), "1w ago"},
264		{"2 weeks", now.Add(-14 * 24 * time.Hour), "02w ago"},
265	}
266
267	for _, tt := range tests {
268		t.Run(tt.name, func(t *testing.T) {
269			result := usecase.GetRelativeTime(tt.t)
270			assert.Equal(t, tt.expected, result)
271		})
272	}
273}

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