9 "github.com/charmbracelet/crush/internal/csync"
10 "github.com/charmbracelet/crush/internal/history"
11 "github.com/charmbracelet/crush/internal/lsp"
12 "github.com/charmbracelet/crush/internal/permission"
13 "github.com/charmbracelet/crush/internal/pubsub"
14 "github.com/stretchr/testify/require"
17type mockPermissionService struct {
18 *pubsub.Broker[permission.PermissionRequest]
21func (m *mockPermissionService) Request(req permission.CreatePermissionRequest) bool {
25func (m *mockPermissionService) Grant(req permission.PermissionRequest) {}
27func (m *mockPermissionService) Deny(req permission.PermissionRequest) {}
29func (m *mockPermissionService) GrantPersistent(req permission.PermissionRequest) {}
31func (m *mockPermissionService) AutoApproveSession(sessionID string) {}
33func (m *mockPermissionService) SetSkipRequests(skip bool) {}
35func (m *mockPermissionService) SkipRequests() bool {
39func (m *mockPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
40 return make(<-chan pubsub.Event[permission.PermissionNotification])
43type mockHistoryService struct {
44 *pubsub.Broker[history.File]
47func (m *mockHistoryService) Create(ctx context.Context, sessionID, path, content string) (history.File, error) {
48 return history.File{Path: path, Content: content}, nil
51func (m *mockHistoryService) CreateVersion(ctx context.Context, sessionID, path, content string) (history.File, error) {
52 return history.File{}, nil
55func (m *mockHistoryService) GetByPathAndSession(ctx context.Context, path, sessionID string) (history.File, error) {
56 return history.File{Path: path, Content: ""}, nil
59func (m *mockHistoryService) Get(ctx context.Context, id string) (history.File, error) {
60 return history.File{}, nil
63func (m *mockHistoryService) ListBySession(ctx context.Context, sessionID string) ([]history.File, error) {
67func (m *mockHistoryService) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]history.File, error) {
71func (m *mockHistoryService) Delete(ctx context.Context, id string) error {
75func (m *mockHistoryService) DeleteSessionFiles(ctx context.Context, sessionID string) error {
79func TestApplyEditToContentPartialSuccess(t *testing.T) {
82 content := "line 1\nline 2\nline 3\n"
84 // Test successful edit.
85 newContent, err := applyEditToContent(content, MultiEditOperation{
89 require.NoError(t, err)
90 require.Contains(t, newContent, "LINE 1")
91 require.Contains(t, newContent, "line 2")
93 // Test failed edit (string not found).
94 _, err = applyEditToContent(content, MultiEditOperation{
99 require.Contains(t, err.Error(), "not found")
102func TestMultiEditSequentialApplication(t *testing.T) {
105 tmpDir := t.TempDir()
106 testFile := filepath.Join(tmpDir, "test.txt")
109 content := "line 1\nline 2\nline 3\nline 4\n"
110 err := os.WriteFile(testFile, []byte(content), 0o644)
111 require.NoError(t, err)
114 lspClients := csync.NewMap[string, *lsp.Client]()
115 permissions := &mockPermissionService{Broker: pubsub.NewBroker[permission.PermissionRequest]()}
116 files := &mockHistoryService{Broker: pubsub.NewBroker[history.File]()}
118 // Create multiedit tool.
119 _ = NewMultiEditTool(lspClients, permissions, files, tmpDir)
121 // Simulate reading the file first.
122 recordFileRead(testFile)
124 // Manually test the sequential application logic.
125 currentContent := content
127 // Apply edits sequentially, tracking failures.
128 edits := []MultiEditOperation{
129 {OldString: "line 1", NewString: "LINE 1"}, // Should succeed
130 {OldString: "line 99", NewString: "LINE 99"}, // Should fail - doesn't exist
131 {OldString: "line 3", NewString: "LINE 3"}, // Should succeed
132 {OldString: "line 2", NewString: "LINE 2"}, // Should succeed - still exists
135 var failedEdits []FailedEdit
138 for i, edit := range edits {
139 newContent, err := applyEditToContent(currentContent, edit)
141 failedEdits = append(failedEdits, FailedEdit{
148 currentContent = newContent
153 require.Equal(t, 3, successCount, "Expected 3 successful edits")
154 require.Len(t, failedEdits, 1, "Expected 1 failed edit")
156 // Check failed edit details.
157 require.Equal(t, 2, failedEdits[0].Index)
158 require.Contains(t, failedEdits[0].Error, "not found")
160 // Verify content changes.
161 require.Contains(t, currentContent, "LINE 1")
162 require.Contains(t, currentContent, "LINE 2")
163 require.Contains(t, currentContent, "LINE 3")
164 require.Contains(t, currentContent, "line 4") // Original unchanged
165 require.NotContains(t, currentContent, "LINE 99")
168func TestMultiEditAllEditsSucceed(t *testing.T) {
171 content := "line 1\nline 2\nline 3\n"
173 edits := []MultiEditOperation{
174 {OldString: "line 1", NewString: "LINE 1"},
175 {OldString: "line 2", NewString: "LINE 2"},
176 {OldString: "line 3", NewString: "LINE 3"},
179 currentContent := content
182 for _, edit := range edits {
183 newContent, err := applyEditToContent(currentContent, edit)
185 t.Fatalf("Unexpected error: %v", err)
187 currentContent = newContent
191 require.Equal(t, 3, successCount)
192 require.Contains(t, currentContent, "LINE 1")
193 require.Contains(t, currentContent, "LINE 2")
194 require.Contains(t, currentContent, "LINE 3")
197func TestMultiEditAllEditsFail(t *testing.T) {
200 content := "line 1\nline 2\n"
202 edits := []MultiEditOperation{
203 {OldString: "line 99", NewString: "LINE 99"},
204 {OldString: "line 100", NewString: "LINE 100"},
207 currentContent := content
208 var failedEdits []FailedEdit
210 for i, edit := range edits {
211 newContent, err := applyEditToContent(currentContent, edit)
213 failedEdits = append(failedEdits, FailedEdit{
220 currentContent = newContent
223 require.Len(t, failedEdits, 2)
224 require.Equal(t, content, currentContent, "Content should be unchanged")
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list