1package snapshot
2
3import (
4 "context"
5 "errors"
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/mock"
14 "github.com/stretchr/testify/require"
15)
16
17type testProvider struct {
18 sessionID entity.SessionID
19 tabList *entity.TabList
20}
21
22func (p *testProvider) GetTabList() *entity.TabList {
23 return p.tabList
24}
25
26func (p *testProvider) GetSessionID() entity.SessionID {
27 return p.sessionID
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, &testProvider{
45 sessionID: "20260207_120000_fk_retry_ok",
46 tabList: entity.NewTabList(),
47 }, 1)
48 svc.retryDelay = time.Millisecond
49 svc.ready = true
50 svc.dirty = true
51
52 err := svc.saveSnapshot(context.Background())
53 require.NoError(t, err)
54 assert.Equal(t, 2, calls)
55 assert.False(t, svc.dirty)
56}
57
58func TestService_SaveSnapshot_RetriesTransientFKAndFails(t *testing.T) {
59 repo := repomocks.NewMockSessionStateRepository(t)
60 calls := 0
61 fkErr := errors.New("save session snapshot: FOREIGN KEY constraint failed")
62 repo.EXPECT().
63 SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
64 RunAndReturn(func(_ context.Context, _ *entity.SessionState) error {
65 calls++
66 return fkErr
67 })
68
69 uc := usecase.NewSnapshotSessionUseCase(repo)
70 svc := NewService(uc, &testProvider{
71 sessionID: "20260207_120000_fk_retry_fail",
72 tabList: entity.NewTabList(),
73 }, 1)
74 svc.retryDelay = time.Millisecond
75 svc.ready = true
76 svc.dirty = true
77
78 err := svc.saveSnapshot(context.Background())
79 require.Error(t, err)
80 require.ErrorIs(t, err, fkErr)
81 assert.Equal(t, svc.retries+1, calls)
82 assert.True(t, svc.dirty)
83}
84
85func TestService_SaveSnapshot_DoesNotRetryNonFKError(t *testing.T) {
86 repo := repomocks.NewMockSessionStateRepository(t)
87 calls := 0
88 nonFKErr := errors.New("database is read-only")
89 repo.EXPECT().
90 SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
91 RunAndReturn(func(_ context.Context, _ *entity.SessionState) error {
92 calls++
93 return nonFKErr
94 })
95
96 uc := usecase.NewSnapshotSessionUseCase(repo)
97 svc := NewService(uc, &testProvider{
98 sessionID: "20260207_120000_non_fk",
99 tabList: entity.NewTabList(),
100 }, 1)
101 svc.retryDelay = time.Millisecond
102 svc.ready = true
103 svc.dirty = true
104
105 err := svc.saveSnapshot(context.Background())
106 require.Error(t, err)
107 require.ErrorIs(t, err, nonFKErr)
108 assert.Equal(t, 1, calls)
109 assert.True(t, svc.dirty)
110}
111
112func TestService_SetReady_SavesPendingDirtySnapshot(t *testing.T) {
113 repo := repomocks.NewMockSessionStateRepository(t)
114 saved := make(chan struct{}, 1)
115 repo.EXPECT().
116 SaveSnapshot(mock.Anything, mock.AnythingOfType("*entity.SessionState")).
117 RunAndReturn(func(_ context.Context, _ *entity.SessionState) error {
118 saved <- struct{}{}
119 return nil
120 })
121
122 uc := usecase.NewSnapshotSessionUseCase(repo)
123 svc := NewService(uc, &testProvider{
124 sessionID: "20260207_120000_ready_flush",
125 tabList: entity.NewTabList(),
126 }, 1)
127 svc.Start(context.Background())
128 svc.dirty = true
129
130 svc.SetReady()
131
132 select {
133 case <-saved:
134 case <-time.After(500 * time.Millisecond):
135 t.Fatal("expected pending snapshot to be saved after SetReady")
136 }
137 svc.mu.Lock()
138 dirty := svc.dirty
139 svc.mu.Unlock()
140 assert.False(t, dirty)
141}