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}