main 5041353

cleanup_sessions_test.go RawHistoryFolder

1package usecase_test
2
3import (
4	"errors"
5	"testing"
6	"time"
7
8	portmocks "github.com/bnema/dumber/internal/application/port/mocks"
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/mock"
14	"github.com/stretchr/testify/require"
15)
16
17func TestCleanupSessionsUseCase_ReconcileActiveBrowserSessions_EndsOnlyProvenDeadSessions(t *testing.T) {
18	ctx := testContext()
19
20	sessionRepo := repomocks.NewMockSessionRepository(t)
21	processProbe := portmocks.NewMockSessionProcessProbe(t)
22
23	now := time.Date(2026, 6, 18, 20, 0, 0, 0, time.UTC)
24	livePID := 111
25	deadPID := 222
26	current := &entity.Session{ID: "current", Type: entity.SessionTypeBrowser, StartedAt: now}
27	live := &entity.Session{ID: "live", Type: entity.SessionTypeBrowser, StartedAt: now.Add(-time.Minute), ProcessID: &livePID}
28	dead := &entity.Session{ID: "dead", Type: entity.SessionTypeBrowser, StartedAt: now.Add(-2 * time.Minute), ProcessID: &deadPID}
29	unknown := &entity.Session{ID: "unknown", Type: entity.SessionTypeBrowser, StartedAt: now.Add(-3 * time.Minute)}
30
31	sessionRepo.EXPECT().
32		GetRecent(ctx, 25).
33		Return([]*entity.Session{current, live, dead, unknown}, nil)
34	processProbe.EXPECT().
35		IsProcessAlive(ctx, livePID).
36		Return(true, nil)
37	processProbe.EXPECT().
38		IsProcessAlive(ctx, deadPID).
39		Return(false, nil)
40	sessionRepo.EXPECT().
41		MarkEnded(ctx, dead.ID, now).
42		Return(nil)
43
44	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, processProbe)
45
46	output, err := uc.ReconcileActiveBrowserSessions(ctx, usecase.ReconcileActiveBrowserSessionsInput{
47		CurrentSessionID: current.ID,
48		RecentLimit:      25,
49		EndedAt:          now,
50	})
51
52	require.NoError(t, err)
53	assert.Equal(t, int64(1), output.EndedDeadSessions)
54}
55
56func TestCleanupSessionsUseCase_ReconcileActiveBrowserSessions_SkipsWhenProbeErrors(t *testing.T) {
57	ctx := testContext()
58
59	sessionRepo := repomocks.NewMockSessionRepository(t)
60	processProbe := portmocks.NewMockSessionProcessProbe(t)
61
62	now := time.Date(2026, 6, 18, 20, 30, 0, 0, time.UTC)
63	pid := 333
64	active := &entity.Session{ID: "active", Type: entity.SessionTypeBrowser, StartedAt: now.Add(-time.Minute), ProcessID: &pid}
65
66	sessionRepo.EXPECT().
67		GetRecent(ctx, 20).
68		Return([]*entity.Session{active}, nil)
69	processProbe.EXPECT().
70		IsProcessAlive(ctx, pid).
71		Return(false, errors.New("probe failed"))
72
73	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, processProbe)
74
75	output, err := uc.ReconcileActiveBrowserSessions(ctx, usecase.ReconcileActiveBrowserSessionsInput{
76		RecentLimit: 20,
77		EndedAt:     now,
78	})
79
80	require.NoError(t, err)
81	assert.Equal(t, int64(0), output.EndedDeadSessions)
82}
83
84func TestCleanupSessionsUseCase_Execute_AgeAndCountCleanup(t *testing.T) {
85	ctx := testContext()
86
87	sessionRepo := repomocks.NewMockSessionRepository(t)
88
89	// Expect age-based deletion
90	sessionRepo.EXPECT().
91		DeleteExitedBefore(ctx, mock.MatchedBy(func(cutoff time.Time) bool {
92			// Should be approximately 7 days ago
93			expected := time.Now().AddDate(0, 0, -7)
94			diff := expected.Sub(cutoff)
95			return diff > -time.Minute && diff < time.Minute
96		})).
97		Return(int64(3), nil)
98
99	// Expect count-based deletion
100	sessionRepo.EXPECT().
101		DeleteOldestExited(ctx, 50).
102		Return(int64(2), nil)
103
104	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
105
106	output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
107		MaxExitedSessions:       50,
108		MaxExitedSessionAgeDays: 7,
109	})
110
111	require.NoError(t, err)
112	assert.Equal(t, int64(3), output.DeletedByAge)
113	assert.Equal(t, int64(2), output.DeletedByCount)
114	assert.Equal(t, int64(5), output.TotalDeleted)
115}
116
117func TestCleanupSessionsUseCase_Execute_AgeOnlyCleanup(t *testing.T) {
118	ctx := testContext()
119
120	sessionRepo := repomocks.NewMockSessionRepository(t)
121
122	// Expect age-based deletion
123	sessionRepo.EXPECT().
124		DeleteExitedBefore(ctx, mock.Anything).
125		Return(int64(5), nil)
126
127	// Count-based with -1 should be skipped (disabled)
128	// MaxExitedSessions >= 0 triggers count cleanup, so use -1 to disable
129	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
130
131	output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
132		MaxExitedSessions:       -1, // Disabled
133		MaxExitedSessionAgeDays: 30,
134	})
135
136	require.NoError(t, err)
137	assert.Equal(t, int64(5), output.DeletedByAge)
138	assert.Equal(t, int64(0), output.DeletedByCount)
139	assert.Equal(t, int64(5), output.TotalDeleted)
140}
141
142func TestCleanupSessionsUseCase_Execute_CountOnlyCleanup(t *testing.T) {
143	ctx := testContext()
144
145	sessionRepo := repomocks.NewMockSessionRepository(t)
146
147	// Count-based deletion only (age disabled with 0)
148	sessionRepo.EXPECT().
149		DeleteOldestExited(ctx, 10).
150		Return(int64(4), nil)
151
152	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
153
154	output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
155		MaxExitedSessions:       10,
156		MaxExitedSessionAgeDays: 0, // Disabled
157	})
158
159	require.NoError(t, err)
160	assert.Equal(t, int64(0), output.DeletedByAge)
161	assert.Equal(t, int64(4), output.DeletedByCount)
162	assert.Equal(t, int64(4), output.TotalDeleted)
163}
164
165func TestCleanupSessionsUseCase_Execute_NoCleanupNeeded(t *testing.T) {
166	ctx := testContext()
167
168	sessionRepo := repomocks.NewMockSessionRepository(t)
169
170	// Nothing to delete
171	sessionRepo.EXPECT().
172		DeleteExitedBefore(ctx, mock.Anything).
173		Return(int64(0), nil)
174
175	sessionRepo.EXPECT().
176		DeleteOldestExited(ctx, 50).
177		Return(int64(0), nil)
178
179	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
180
181	output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
182		MaxExitedSessions:       50,
183		MaxExitedSessionAgeDays: 7,
184	})
185
186	require.NoError(t, err)
187	assert.Equal(t, int64(0), output.DeletedByAge)
188	assert.Equal(t, int64(0), output.DeletedByCount)
189	assert.Equal(t, int64(0), output.TotalDeleted)
190}
191
192func TestCleanupSessionsUseCase_Execute_AgeDeleteError_ContinuesToCount(t *testing.T) {
193	ctx := testContext()
194
195	sessionRepo := repomocks.NewMockSessionRepository(t)
196
197	// Age-based deletion fails
198	sessionRepo.EXPECT().
199		DeleteExitedBefore(ctx, mock.Anything).
200		Return(int64(0), errors.New("db error"))
201
202	// Should still attempt count-based deletion
203	sessionRepo.EXPECT().
204		DeleteOldestExited(ctx, 50).
205		Return(int64(3), nil)
206
207	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
208
209	output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
210		MaxExitedSessions:       50,
211		MaxExitedSessionAgeDays: 7,
212	})
213
214	// Should not return error, just log warning
215	require.NoError(t, err)
216	assert.Equal(t, int64(0), output.DeletedByAge)
217	assert.Equal(t, int64(3), output.DeletedByCount)
218	assert.Equal(t, int64(3), output.TotalDeleted)
219}
220
221func TestCleanupSessionsUseCase_Execute_CountDeleteError(t *testing.T) {
222	ctx := testContext()
223
224	sessionRepo := repomocks.NewMockSessionRepository(t)
225
226	// Age-based deletion succeeds
227	sessionRepo.EXPECT().
228		DeleteExitedBefore(ctx, mock.Anything).
229		Return(int64(2), nil)
230
231	// Count-based deletion fails
232	sessionRepo.EXPECT().
233		DeleteOldestExited(ctx, 50).
234		Return(int64(0), errors.New("db error"))
235
236	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
237
238	output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
239		MaxExitedSessions:       50,
240		MaxExitedSessionAgeDays: 7,
241	})
242
243	// Should not return error, just log warning
244	require.NoError(t, err)
245	assert.Equal(t, int64(2), output.DeletedByAge)
246	assert.Equal(t, int64(0), output.DeletedByCount)
247	assert.Equal(t, int64(2), output.TotalDeleted)
248}
249
250func TestCleanupSessionsUseCase_Execute_BothDisabled(t *testing.T) {
251	ctx := testContext()
252
253	sessionRepo := repomocks.NewMockSessionRepository(t)
254
255	// No expectations - nothing should be called
256	// Actually, count with 0 means "keep 0" which will trigger cleanup
257	// So we need -1 for count and 0 for age to fully disable
258
259	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
260
261	output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
262		MaxExitedSessions:       -1, // Disabled
263		MaxExitedSessionAgeDays: 0,  // Disabled
264	})
265
266	require.NoError(t, err)
267	assert.Equal(t, int64(0), output.DeletedByAge)
268	assert.Equal(t, int64(0), output.DeletedByCount)
269	assert.Equal(t, int64(0), output.TotalDeleted)
270}
271
272func TestCleanupSessionsUseCase_Execute_ZeroMaxSessions(t *testing.T) {
273	ctx := testContext()
274
275	sessionRepo := repomocks.NewMockSessionRepository(t)
276
277	// MaxExitedSessions = 0 means keep 0 sessions (delete all)
278	sessionRepo.EXPECT().
279		DeleteOldestExited(ctx, 0).
280		Return(int64(10), nil)
281
282	uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
283
284	output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
285		MaxExitedSessions:       0, // Keep 0 = delete all
286		MaxExitedSessionAgeDays: 0, // Disabled
287	})
288
289	require.NoError(t, err)
290	assert.Equal(t, int64(0), output.DeletedByAge)
291	assert.Equal(t, int64(10), output.DeletedByCount)
292	assert.Equal(t, int64(10), output.TotalDeleted)
293}

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