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