v0.31.0 bf0756b

service_test.go RawHistoryFolder

1package snapshot
2
3import (
4	"context"
5	"errors"
6	"testing"
7	"time"
8
9	"github.com/bnema/dumber/internal/application/port/mocks"
10	"github.com/bnema/dumber/internal/application/usecase"
11	"github.com/bnema/dumber/internal/domain/entity"
12	repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/mock"
15	"github.com/stretchr/testify/require"
16)
17
18func newWindowStateProvider(
19	t *testing.T,
20	sessionID entity.SessionID,
21	windows []entity.WindowTabListState,
22	activeWindowIndex int,
23) *mocks.MockWindowStateProvider {
24	provider := mocks.NewMockWindowStateProvider(t)
25	provider.EXPECT().GetSessionID().Return(sessionID).Once()
26	provider.EXPECT().GetWindowSnapshotState().Return(windows, activeWindowIndex).Once()
27	return provider
28}
29
30func TestService_SaveSnapshot_RetriesTransientFKAndSucceeds(t *testing.T) {
31	repo := repomocks.NewMockSessionStateRepository(t)
32	calls := 0
33	repo.EXPECT().
34		SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
35		RunAndReturn(func(_ context.Context, _ *entity.SessionState) error {
36			calls++
37			if calls == 1 {
38				return errors.New("FOREIGN KEY constraint failed")
39			}
40			return nil
41		})
42
43	uc := usecase.NewSnapshotSessionUseCase(repo)
44	svc := NewService(uc, newWindowStateProvider(t, "20260207_120000_fk_retry_ok", nil, 0), 1)
45	svc.retryDelay = time.Millisecond
46	svc.ready = true
47	svc.dirty = true
48
49	err := svc.saveSnapshot(context.Background())
50	require.NoError(t, err)
51	assert.Equal(t, 2, calls)
52	assert.False(t, svc.dirty)
53}
54
55func TestService_SaveSnapshot_RetriesTransientFKAndFails(t *testing.T) {
56	repo := repomocks.NewMockSessionStateRepository(t)
57	calls := 0
58	fkErr := errors.New("save session snapshot: FOREIGN KEY constraint failed")
59	repo.EXPECT().
60		SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
61		RunAndReturn(func(_ context.Context, _ *entity.SessionState) error {
62			calls++
63			return fkErr
64		})
65
66	uc := usecase.NewSnapshotSessionUseCase(repo)
67	svc := NewService(uc, newWindowStateProvider(t, "20260207_120000_fk_retry_fail", nil, 0), 1)
68	svc.retryDelay = time.Millisecond
69	svc.ready = true
70	svc.dirty = true
71
72	err := svc.saveSnapshot(context.Background())
73	require.Error(t, err)
74	require.ErrorIs(t, err, fkErr)
75	assert.Equal(t, svc.retries+1, calls)
76	assert.True(t, svc.dirty)
77}
78
79func TestService_SaveSnapshot_DoesNotRetryNonFKError(t *testing.T) {
80	repo := repomocks.NewMockSessionStateRepository(t)
81	calls := 0
82	nonFKErr := errors.New("database is read-only")
83	repo.EXPECT().
84		SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
85		RunAndReturn(func(_ context.Context, _ *entity.SessionState) error {
86			calls++
87			return nonFKErr
88		})
89
90	uc := usecase.NewSnapshotSessionUseCase(repo)
91	svc := NewService(uc, newWindowStateProvider(t, "20260207_120000_non_fk", nil, 0), 1)
92	svc.retryDelay = time.Millisecond
93	svc.ready = true
94	svc.dirty = true
95
96	err := svc.saveSnapshot(context.Background())
97	require.Error(t, err)
98	require.ErrorIs(t, err, nonFKErr)
99	assert.Equal(t, 1, calls)
100	assert.True(t, svc.dirty)
101}
102
103func TestService_SetReady_SavesPendingDirtySnapshot(t *testing.T) {
104	repo := repomocks.NewMockSessionStateRepository(t)
105	saved := make(chan struct{}, 1)
106	repo.EXPECT().
107		SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
108		RunAndReturn(func(_ context.Context, _ *entity.SessionState) error {
109			saved <- struct{}{}
110			return nil
111		})
112
113	uc := usecase.NewSnapshotSessionUseCase(repo)
114	svc := NewService(uc, newWindowStateProvider(t, "20260207_120000_ready_flush", nil, 0), 1)
115	svc.Start(context.Background())
116	svc.dirty = true
117
118	svc.SetReady()
119
120	select {
121	case <-saved:
122	case <-time.After(500 * time.Millisecond):
123		t.Fatal("expected pending snapshot to be saved after SetReady")
124	}
125	svc.mu.Lock()
126	dirty := svc.dirty
127	svc.mu.Unlock()
128	assert.False(t, dirty)
129}
130
131func TestService_SaveNowPassesWindowSnapshots(t *testing.T) {
132	repo := repomocks.NewMockSessionStateRepository(t)
133
134	pane1 := entity.NewPane("p_w1")
135	pane1.URI = "https://win1.com"
136	tab1_1 := entity.NewTab("t1_w1", "ws1_w1", pane1)
137	tab1_1.Name = "Win1Tab"
138	tabs1 := entity.NewTabList()
139	tabs1.Add(tab1_1)
140
141	pane2 := entity.NewPane("p_w2")
142	pane2.URI = "https://win2.com"
143	tab2_1 := entity.NewTab("t1_w2", "ws1_w2", pane2)
144	tab2_1.Name = "Win2Tab"
145	tabs2 := entity.NewTabList()
146	tabs2.Add(tab2_1)
147
148	windows := []entity.WindowTabListState{
149		{WindowID: "w1", Tabs: tabs1},
150		{WindowID: "w2", Tabs: tabs2},
151	}
152
153	sessionID := entity.SessionID("20260501_window_snap")
154
155	repo.EXPECT().
156		SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
157		Run(func(_ context.Context, state *entity.SessionState) {
158			require.Equal(t, sessionID, state.SessionID)
159			require.Equal(t, 2, state.Version)
160			require.Len(t, state.Windows, 2)
161			assert.Equal(t, entity.WindowID("w1"), state.Windows[0].ID)
162			assert.Equal(t, entity.WindowID("w2"), state.Windows[1].ID)
163			assert.Equal(t, 1, state.ActiveWindowIndex)
164			assert.Empty(t, state.Tabs)
165		}).
166		Return(nil)
167
168	uc := usecase.NewSnapshotSessionUseCase(repo)
169	provider := newWindowStateProvider(t, sessionID, windows, 1)
170	svc := NewService(uc, provider, 1)
171	svc.ready = true
172	svc.dirty = true
173
174	err := svc.saveSnapshot(context.Background())
175	require.NoError(t, err)
176	assert.False(t, svc.dirty)
177}
178
179func TestService_SaveSnapshotKeepsDirtyWhenWindowSnapshotUnavailable(t *testing.T) {
180	repo := repomocks.NewMockSessionStateRepository(t)
181	uc := usecase.NewSnapshotSessionUseCase(repo)
182	provider := mocks.NewMockWindowStateProvider(t)
183	provider.EXPECT().GetSessionID().Return(entity.SessionID("20260501_snapshot_unavailable")).Once()
184	provider.EXPECT().GetWindowSnapshotState().Return(nil, -1).Once()
185
186	svc := NewService(uc, provider, 1)
187	svc.ready = true
188	svc.dirty = true
189
190	err := svc.saveSnapshot(context.Background())
191	require.NoError(t, err)
192	assert.True(t, svc.dirty)
193}
194
195func TestService_SaveNowPersistsEmptyWindowSnapshotAsV2(t *testing.T) {
196	repo := repomocks.NewMockSessionStateRepository(t)
197	sessionID := entity.SessionID("20260501_empty_window_snap")
198
199	repo.EXPECT().
200		SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
201		Run(func(_ context.Context, state *entity.SessionState) {
202			require.Equal(t, sessionID, state.SessionID)
203			require.Equal(t, entity.SessionStateVersion, state.Version)
204			require.Empty(t, state.Windows)
205			require.Empty(t, state.Tabs)
206			assert.Equal(t, 0, state.ActiveWindowIndex)
207		}).
208		Return(nil)
209
210	uc := usecase.NewSnapshotSessionUseCase(repo)
211	provider := newWindowStateProvider(t, sessionID, []entity.WindowTabListState{}, 2)
212	svc := NewService(uc, provider, 1)
213	svc.ready = true
214	svc.dirty = true
215
216	err := svc.saveSnapshot(context.Background())
217	require.NoError(t, err)
218	assert.False(t, svc.dirty)
219}
220
221func TestService_SaveNowPreservesLegacySingleEmptyWindowSentinel(t *testing.T) {
222	repo := repomocks.NewMockSessionStateRepository(t)
223	sessionID := entity.SessionID("20260501_legacy_sentinel")
224
225	pane := entity.NewPane("p_legacy")
226	pane.URI = "https://legacy.example"
227	tab := entity.NewTab("t_legacy", "ws_legacy", pane)
228	tab.Name = "Legacy Tab"
229	tabs := entity.NewTabList()
230	tabs.Add(tab)
231
232	repo.EXPECT().
233		SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
234		Run(func(_ context.Context, state *entity.SessionState) {
235			require.Equal(t, sessionID, state.SessionID)
236			require.Equal(t, entity.LegacySessionStateVersion, state.Version)
237			require.Len(t, state.Tabs, 1)
238			assert.Equal(t, "Legacy Tab", state.Tabs[0].Name)
239			assert.Empty(t, state.Windows)
240			assert.Equal(t, 0, state.ActiveWindowIndex)
241		}).
242		Return(nil)
243
244	uc := usecase.NewSnapshotSessionUseCase(repo)
245	provider := newWindowStateProvider(t, sessionID, []entity.WindowTabListState{{WindowID: "", Tabs: tabs}}, 3)
246	svc := NewService(uc, provider, 1)
247	svc.ready = true
248	svc.dirty = true
249
250	err := svc.saveSnapshot(context.Background())
251	require.NoError(t, err)
252	assert.False(t, svc.dirty)
253}

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