1package tools
2
3import (
4 "context"
5 "fmt"
6 "sort"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/google/uuid"
12 "github.com/kujtimiihoxha/opencode/internal/history"
13 "github.com/kujtimiihoxha/opencode/internal/permission"
14 "github.com/kujtimiihoxha/opencode/internal/pubsub"
15)
16
17// Mock permission service for testing
18type mockPermissionService struct {
19 *pubsub.Broker[permission.PermissionRequest]
20 allow bool
21}
22
23func (m *mockPermissionService) GrantPersistant(permission permission.PermissionRequest) {
24 // Not needed for tests
25}
26
27func (m *mockPermissionService) Grant(permission permission.PermissionRequest) {
28 // Not needed for tests
29}
30
31func (m *mockPermissionService) Deny(permission permission.PermissionRequest) {
32 // Not needed for tests
33}
34
35func (m *mockPermissionService) Request(opts permission.CreatePermissionRequest) bool {
36 return m.allow
37}
38
39func newMockPermissionService(allow bool) permission.Service {
40 return &mockPermissionService{
41 Broker: pubsub.NewBroker[permission.PermissionRequest](),
42 allow: allow,
43 }
44}
45
46type mockFileHistoryService struct {
47 *pubsub.Broker[history.File]
48 files map[string]history.File // ID -> File
49 timeNow func() int64
50}
51
52// Create implements history.Service.
53func (m *mockFileHistoryService) Create(ctx context.Context, sessionID string, path string, content string) (history.File, error) {
54 return m.createWithVersion(ctx, sessionID, path, content, history.InitialVersion)
55}
56
57// CreateVersion implements history.Service.
58func (m *mockFileHistoryService) CreateVersion(ctx context.Context, sessionID string, path string, content string) (history.File, error) {
59 var files []history.File
60 for _, file := range m.files {
61 if file.Path == path {
62 files = append(files, file)
63 }
64 }
65
66 if len(files) == 0 {
67 // No previous versions, create initial
68 return m.Create(ctx, sessionID, path, content)
69 }
70
71 // Sort files by CreatedAt in descending order
72 sort.Slice(files, func(i, j int) bool {
73 return files[i].CreatedAt > files[j].CreatedAt
74 })
75
76 // Get the latest version
77 latestFile := files[0]
78 latestVersion := latestFile.Version
79
80 // Generate the next version
81 var nextVersion string
82 if latestVersion == history.InitialVersion {
83 nextVersion = "v1"
84 } else if strings.HasPrefix(latestVersion, "v") {
85 versionNum, err := strconv.Atoi(latestVersion[1:])
86 if err != nil {
87 // If we can't parse the version, just use a timestamp-based version
88 nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
89 } else {
90 nextVersion = fmt.Sprintf("v%d", versionNum+1)
91 }
92 } else {
93 // If the version format is unexpected, use a timestamp-based version
94 nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
95 }
96
97 return m.createWithVersion(ctx, sessionID, path, content, nextVersion)
98}
99
100func (m *mockFileHistoryService) createWithVersion(_ context.Context, sessionID, path, content, version string) (history.File, error) {
101 now := m.timeNow()
102 file := history.File{
103 ID: uuid.New().String(),
104 SessionID: sessionID,
105 Path: path,
106 Content: content,
107 Version: version,
108 CreatedAt: now,
109 UpdatedAt: now,
110 }
111
112 m.files[file.ID] = file
113 m.Publish(pubsub.CreatedEvent, file)
114 return file, nil
115}
116
117// Delete implements history.Service.
118func (m *mockFileHistoryService) Delete(ctx context.Context, id string) error {
119 file, ok := m.files[id]
120 if !ok {
121 return fmt.Errorf("file not found: %s", id)
122 }
123
124 delete(m.files, id)
125 m.Publish(pubsub.DeletedEvent, file)
126 return nil
127}
128
129// DeleteSessionFiles implements history.Service.
130func (m *mockFileHistoryService) DeleteSessionFiles(ctx context.Context, sessionID string) error {
131 files, err := m.ListBySession(ctx, sessionID)
132 if err != nil {
133 return err
134 }
135
136 for _, file := range files {
137 err = m.Delete(ctx, file.ID)
138 if err != nil {
139 return err
140 }
141 }
142
143 return nil
144}
145
146// Get implements history.Service.
147func (m *mockFileHistoryService) Get(ctx context.Context, id string) (history.File, error) {
148 file, ok := m.files[id]
149 if !ok {
150 return history.File{}, fmt.Errorf("file not found: %s", id)
151 }
152 return file, nil
153}
154
155// GetByPathAndSession implements history.Service.
156func (m *mockFileHistoryService) GetByPathAndSession(ctx context.Context, path string, sessionID string) (history.File, error) {
157 var latestFile history.File
158 var found bool
159 var latestTime int64
160
161 for _, file := range m.files {
162 if file.Path == path && file.SessionID == sessionID {
163 if !found || file.CreatedAt > latestTime {
164 latestFile = file
165 latestTime = file.CreatedAt
166 found = true
167 }
168 }
169 }
170
171 if !found {
172 return history.File{}, fmt.Errorf("file not found: %s for session %s", path, sessionID)
173 }
174 return latestFile, nil
175}
176
177// ListBySession implements history.Service.
178func (m *mockFileHistoryService) ListBySession(ctx context.Context, sessionID string) ([]history.File, error) {
179 var files []history.File
180 for _, file := range m.files {
181 if file.SessionID == sessionID {
182 files = append(files, file)
183 }
184 }
185
186 // Sort by CreatedAt in descending order
187 sort.Slice(files, func(i, j int) bool {
188 return files[i].CreatedAt > files[j].CreatedAt
189 })
190
191 return files, nil
192}
193
194// ListLatestSessionFiles implements history.Service.
195func (m *mockFileHistoryService) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]history.File, error) {
196 // Map to track the latest file for each path
197 latestFiles := make(map[string]history.File)
198
199 for _, file := range m.files {
200 if file.SessionID == sessionID {
201 existing, ok := latestFiles[file.Path]
202 if !ok || file.CreatedAt > existing.CreatedAt {
203 latestFiles[file.Path] = file
204 }
205 }
206 }
207
208 // Convert map to slice
209 var result []history.File
210 for _, file := range latestFiles {
211 result = append(result, file)
212 }
213
214 // Sort by CreatedAt in descending order
215 sort.Slice(result, func(i, j int) bool {
216 return result[i].CreatedAt > result[j].CreatedAt
217 })
218
219 return result, nil
220}
221
222// Subscribe implements history.Service.
223func (m *mockFileHistoryService) Subscribe(ctx context.Context) <-chan pubsub.Event[history.File] {
224 return m.Broker.Subscribe(ctx)
225}
226
227// Update implements history.Service.
228func (m *mockFileHistoryService) Update(ctx context.Context, file history.File) (history.File, error) {
229 _, ok := m.files[file.ID]
230 if !ok {
231 return history.File{}, fmt.Errorf("file not found: %s", file.ID)
232 }
233
234 file.UpdatedAt = m.timeNow()
235 m.files[file.ID] = file
236 m.Publish(pubsub.UpdatedEvent, file)
237 return file, nil
238}
239
240func newMockFileHistoryService() history.Service {
241 return &mockFileHistoryService{
242 Broker: pubsub.NewBroker[history.File](),
243 files: make(map[string]history.File),
244 timeNow: func() int64 { return time.Now().Unix() },
245 }
246}