1package tools
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/charmbracelet/crush/internal/history"
10 "github.com/charmbracelet/crush/internal/permission"
11 "github.com/charmbracelet/crush/internal/pubsub"
12 "github.com/stretchr/testify/require"
13)
14
15type mockPermissionService struct {
16 *pubsub.Broker[permission.PermissionRequest]
17}
18
19func (m *mockPermissionService) Request(ctx context.Context, req permission.CreatePermissionRequest) (bool, error) {
20 return true, nil
21}
22
23func (m *mockPermissionService) Grant(req permission.PermissionRequest) bool { return true }
24
25func (m *mockPermissionService) Deny(req permission.PermissionRequest) bool { return true }
26
27func (m *mockPermissionService) GrantPersistent(req permission.PermissionRequest) bool {
28 return true
29}
30
31func (m *mockPermissionService) AutoApproveSession(sessionID string) {}
32
33func (m *mockPermissionService) SetSkipRequests(skip bool) {}
34
35func (m *mockPermissionService) SkipRequests() bool {
36 return false
37}
38
39func (m *mockPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
40 return make(<-chan pubsub.Event[permission.PermissionNotification])
41}
42
43type mockHistoryService struct {
44 *pubsub.Broker[history.File]
45}
46
47func (m *mockHistoryService) Create(ctx context.Context, sessionID, path, content string) (history.File, error) {
48 return history.File{Path: path, Content: content}, nil
49}
50
51func (m *mockHistoryService) CreateVersion(ctx context.Context, sessionID, path, content string) (history.File, error) {
52 return history.File{}, nil
53}
54
55func (m *mockHistoryService) GetByPathAndSession(ctx context.Context, path, sessionID string) (history.File, error) {
56 return history.File{Path: path, Content: ""}, nil
57}
58
59func (m *mockHistoryService) Get(ctx context.Context, id string) (history.File, error) {
60 return history.File{}, nil
61}
62
63func (m *mockHistoryService) ListBySession(ctx context.Context, sessionID string) ([]history.File, error) {
64 return nil, nil
65}
66
67func (m *mockHistoryService) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]history.File, error) {
68 return nil, nil
69}
70
71func (m *mockHistoryService) Delete(ctx context.Context, id string) error {
72 return nil
73}
74
75func (m *mockHistoryService) DeleteSessionFiles(ctx context.Context, sessionID string) error {
76 return nil
77}
78
79func TestApplyEditToContentPartialSuccess(t *testing.T) {
80 t.Parallel()
81
82 content := "line 1\nline 2\nline 3\n"
83
84 // Test successful edit.
85 newContent, err := applyEditToContent(content, MultiEditOperation{
86 OldString: "line 1",
87 NewString: "LINE 1",
88 })
89 require.NoError(t, err)
90 require.Contains(t, newContent, "LINE 1")
91 require.Contains(t, newContent, "line 2")
92
93 // Test failed edit (string not found).
94 _, err = applyEditToContent(content, MultiEditOperation{
95 OldString: "line 99",
96 NewString: "LINE 99",
97 })
98 require.Error(t, err)
99 require.Contains(t, err.Error(), "not found")
100}
101
102func TestMultiEditSequentialApplication(t *testing.T) {
103 t.Parallel()
104
105 tmpDir := t.TempDir()
106 testFile := filepath.Join(tmpDir, "test.txt")
107
108 // Create test file.
109 content := "line 1\nline 2\nline 3\nline 4\n"
110 err := os.WriteFile(testFile, []byte(content), 0o644)
111 require.NoError(t, err)
112
113 // Manually test the sequential application logic.
114 currentContent := content
115
116 // Apply edits sequentially, tracking failures.
117 edits := []MultiEditOperation{
118 {OldString: "line 1", NewString: "LINE 1"}, // Should succeed
119 {OldString: "line 99", NewString: "LINE 99"}, // Should fail - doesn't exist
120 {OldString: "line 3", NewString: "LINE 3"}, // Should succeed
121 {OldString: "line 2", NewString: "LINE 2"}, // Should succeed - still exists
122 }
123
124 var failedEdits []FailedEdit
125 successCount := 0
126
127 for i, edit := range edits {
128 newContent, err := applyEditToContent(currentContent, edit)
129 if err != nil {
130 failedEdits = append(failedEdits, FailedEdit{
131 Index: i + 1,
132 Error: err.Error(),
133 Edit: edit,
134 })
135 continue
136 }
137 currentContent = newContent
138 successCount++
139 }
140
141 // Verify results.
142 require.Equal(t, 3, successCount, "Expected 3 successful edits")
143 require.Len(t, failedEdits, 1, "Expected 1 failed edit")
144
145 // Check failed edit details.
146 require.Equal(t, 2, failedEdits[0].Index)
147 require.Contains(t, failedEdits[0].Error, "not found")
148
149 // Verify content changes.
150 require.Contains(t, currentContent, "LINE 1")
151 require.Contains(t, currentContent, "LINE 2")
152 require.Contains(t, currentContent, "LINE 3")
153 require.Contains(t, currentContent, "line 4") // Original unchanged
154 require.NotContains(t, currentContent, "LINE 99")
155}
156
157func TestMultiEditAllEditsSucceed(t *testing.T) {
158 t.Parallel()
159
160 content := "line 1\nline 2\nline 3\n"
161
162 edits := []MultiEditOperation{
163 {OldString: "line 1", NewString: "LINE 1"},
164 {OldString: "line 2", NewString: "LINE 2"},
165 {OldString: "line 3", NewString: "LINE 3"},
166 }
167
168 currentContent := content
169 successCount := 0
170
171 for _, edit := range edits {
172 newContent, err := applyEditToContent(currentContent, edit)
173 if err != nil {
174 t.Fatalf("Unexpected error: %v", err)
175 }
176 currentContent = newContent
177 successCount++
178 }
179
180 require.Equal(t, 3, successCount)
181 require.Contains(t, currentContent, "LINE 1")
182 require.Contains(t, currentContent, "LINE 2")
183 require.Contains(t, currentContent, "LINE 3")
184}
185
186func TestMultiEditAllEditsFail(t *testing.T) {
187 t.Parallel()
188
189 content := "line 1\nline 2\n"
190
191 edits := []MultiEditOperation{
192 {OldString: "line 99", NewString: "LINE 99"},
193 {OldString: "line 100", NewString: "LINE 100"},
194 }
195
196 currentContent := content
197 var failedEdits []FailedEdit
198
199 for i, edit := range edits {
200 newContent, err := applyEditToContent(currentContent, edit)
201 if err != nil {
202 failedEdits = append(failedEdits, FailedEdit{
203 Index: i + 1,
204 Error: err.Error(),
205 Edit: edit,
206 })
207 continue
208 }
209 currentContent = newContent
210 }
211
212 require.Len(t, failedEdits, 2)
213 require.Equal(t, content, currentContent, "Content should be unchanged")
214}