multiedit_test.go

  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}