fileditor-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/docs/MCP-INTERFACE.cn.md +976 -0
- package/docs/MCP-INTERFACE.en.md +881 -0
- package/docs/README.cn.md +128 -0
- package/package.json +27 -0
- package/src/handlers/applyDiff.js +298 -0
- package/src/handlers/insertContent.js +179 -0
- package/src/handlers/listFiles.js +65 -0
- package/src/handlers/readFile.js +109 -0
- package/src/handlers/searchAndReplace.js +198 -0
- package/src/handlers/setWorkspace.js +26 -0
- package/src/handlers/writeFile.js +98 -0
- package/src/index.js +20 -0
- package/src/server.js +85 -0
- package/src/tools/toolDefinitions.js +291 -0
- package/src/utils/fileUtils.js +238 -0
- package/test/ApplyDiffHandler.test.js +754 -0
- package/test/InsertContentHandler.test.js +371 -0
- package/test/ListFilesHandler.test.js +302 -0
- package/test/ReadFileHandler.test.js +213 -0
- package/test/SearchAndReplaceHandler.test.js +505 -0
- package/test/SetWorkspaceHandler.test.js +290 -0
- package/test/WriteFileHandler.test.js +289 -0
- package/test/runAllTests.js +233 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import { ApplyDiffHandler } from '../src/handlers/applyDiff.js';
|
|
2
|
+
import { FileUtils } from '../src/utils/fileUtils.js';
|
|
3
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ApplyDiffHandler test class
|
|
11
|
+
* Test precise find and replace functionality
|
|
12
|
+
*/
|
|
13
|
+
describe('ApplyDiffHandler', () => {
|
|
14
|
+
|
|
15
|
+
const testDir = './test_files_diff';
|
|
16
|
+
const testFile1 = join(testDir, 'test1.txt');
|
|
17
|
+
const baseContent = `function example() {
|
|
18
|
+
console.log("Hello World");
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class TestClass {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.value = 42;
|
|
25
|
+
}
|
|
26
|
+
}`;
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
// Set up test workspace
|
|
30
|
+
FileUtils.setWorkspaceRoot(process.cwd());
|
|
31
|
+
|
|
32
|
+
// Create test directory and files
|
|
33
|
+
if (!existsSync(testDir)) {
|
|
34
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
await fs.writeFile(testFile1, baseContent, 'utf8');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
// Clean up test files
|
|
41
|
+
try {
|
|
42
|
+
if (existsSync(testFile1)) await fs.unlink(testFile1);
|
|
43
|
+
if (existsSync(testDir)) await fs.rmdir(testDir);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn('Failed to clean up test files:', error.message);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Basic Functionality', () => {
|
|
50
|
+
it('should successfully replace matching content', async () => {
|
|
51
|
+
const args = {
|
|
52
|
+
path: testFile1,
|
|
53
|
+
search_content: ' console.log("Hello World");', // with correct indentation
|
|
54
|
+
replace_content: ' console.log("Hello Universe");',
|
|
55
|
+
start_line: 2
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
59
|
+
|
|
60
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
61
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
62
|
+
assert.ok(result.content[0].text.includes('Replaced 1 line(s)'));
|
|
63
|
+
|
|
64
|
+
// Verify file content
|
|
65
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
66
|
+
assert.ok(content.includes('console.log("Hello Universe");'));
|
|
67
|
+
assert.ok(!content.includes('console.log("Hello World");'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should successfully replace multi-line content', async () => {
|
|
71
|
+
const searchContent = `class TestClass {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.value = 42;
|
|
74
|
+
}
|
|
75
|
+
}`;
|
|
76
|
+
const replaceContent = `class TestClass {
|
|
77
|
+
constructor(value = 0) {
|
|
78
|
+
this.value = value;
|
|
79
|
+
this.name = "test";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getValue() {
|
|
83
|
+
return this.value;
|
|
84
|
+
}
|
|
85
|
+
}`;
|
|
86
|
+
|
|
87
|
+
const args = {
|
|
88
|
+
path: testFile1,
|
|
89
|
+
search_content: searchContent,
|
|
90
|
+
replace_content: replaceContent,
|
|
91
|
+
start_line: 6
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
95
|
+
|
|
96
|
+
assert.strictEqual(result.content[0].type, 'text'); assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
97
|
+
assert.ok(result.content[0].text.includes('added 5 line(s)')); // Corrected expectation
|
|
98
|
+
|
|
99
|
+
// Verify file content
|
|
100
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
101
|
+
assert.ok(content.includes('this.name = "test";'));
|
|
102
|
+
assert.ok(content.includes('getValue()'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle line deletions', async () => {
|
|
106
|
+
const searchContent = `function example() {
|
|
107
|
+
console.log("Hello World");
|
|
108
|
+
return true;
|
|
109
|
+
}`;
|
|
110
|
+
const replaceContent = `function example() {
|
|
111
|
+
return true;
|
|
112
|
+
}`;
|
|
113
|
+
|
|
114
|
+
const args = {
|
|
115
|
+
path: testFile1,
|
|
116
|
+
search_content: searchContent,
|
|
117
|
+
replace_content: replaceContent,
|
|
118
|
+
start_line: 1
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
122
|
+
|
|
123
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
124
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
125
|
+
assert.ok(result.content[0].text.includes('removed 1 line(s)'));
|
|
126
|
+
|
|
127
|
+
// Verify file content
|
|
128
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
129
|
+
assert.ok(!content.includes('console.log("Hello World");'));
|
|
130
|
+
assert.ok(content.includes('return true;'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('Error Handling', () => {
|
|
136
|
+
|
|
137
|
+
it('should return an error when the file does not exist', async () => {
|
|
138
|
+
const args = {
|
|
139
|
+
path: './nonexistent.txt',
|
|
140
|
+
search_content: 'some content',
|
|
141
|
+
replace_content: 'new content',
|
|
142
|
+
start_line: 1
|
|
143
|
+
};
|
|
144
|
+
await assert.rejects(
|
|
145
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
146
|
+
(error) => {
|
|
147
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
148
|
+
assert.strictEqual(result.isError, true);
|
|
149
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
150
|
+
assert.ok(result.content[0].text.includes('Error: File not found'));
|
|
151
|
+
return true;
|
|
152
|
+
},
|
|
153
|
+
'should throw an error when the file does not exist'
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return an error for an invalid start line number', async () => {
|
|
158
|
+
const args = {
|
|
159
|
+
path: testFile1,
|
|
160
|
+
search_content: 'some content',
|
|
161
|
+
replace_content: 'new content',
|
|
162
|
+
start_line: 0
|
|
163
|
+
};
|
|
164
|
+
await assert.rejects(
|
|
165
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
166
|
+
(error) => {
|
|
167
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
168
|
+
assert.strictEqual(result.isError, true);
|
|
169
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
170
|
+
assert.ok(result.content[0].text.includes('Error: Invalid start_line'));
|
|
171
|
+
return true;
|
|
172
|
+
},
|
|
173
|
+
'should throw an error for an invalid start line number'
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should return an error when the start line is out of bounds', async () => {
|
|
178
|
+
const args = {
|
|
179
|
+
path: testFile1,
|
|
180
|
+
search_content: 'some content',
|
|
181
|
+
replace_content: 'new content',
|
|
182
|
+
start_line: 100
|
|
183
|
+
};
|
|
184
|
+
await assert.rejects(
|
|
185
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
186
|
+
(error) => {
|
|
187
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
188
|
+
assert.strictEqual(result.isError, true);
|
|
189
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
190
|
+
assert.ok(result.content[0].text.includes('Error: Single diff failed'));
|
|
191
|
+
assert.ok(result.content[0].text.includes('exceeds file length'));
|
|
192
|
+
return true;
|
|
193
|
+
},
|
|
194
|
+
'should throw an error when the start line is out of bounds'
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return an error when search content exceeds file bounds', async () => {
|
|
199
|
+
const longSearchContent = Array.from({ length: 50 }, (_, i) => `Line ${i + 1}`).join('\n');
|
|
200
|
+
const args = {
|
|
201
|
+
path: testFile1,
|
|
202
|
+
search_content: longSearchContent,
|
|
203
|
+
replace_content: 'replacement',
|
|
204
|
+
start_line: 5
|
|
205
|
+
};
|
|
206
|
+
await assert.rejects(
|
|
207
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
208
|
+
(error) => {
|
|
209
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
210
|
+
assert.strictEqual(result.isError, true);
|
|
211
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
212
|
+
assert.ok(result.content[0].text.includes('Error: Single diff failed'));
|
|
213
|
+
assert.ok(result.content[0].text.includes('extends beyond file length'));
|
|
214
|
+
return true;
|
|
215
|
+
},
|
|
216
|
+
'should throw an error when search content exceeds file bounds'
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should return an error for content mismatch', async () => {
|
|
221
|
+
const args = {
|
|
222
|
+
path: testFile1,
|
|
223
|
+
search_content: 'this content does not exist in file',
|
|
224
|
+
replace_content: 'new content',
|
|
225
|
+
start_line: 2
|
|
226
|
+
};
|
|
227
|
+
await assert.rejects(
|
|
228
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
229
|
+
(error) => {
|
|
230
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
231
|
+
assert.strictEqual(result.isError, true);
|
|
232
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
233
|
+
assert.ok(result.content[0].text.includes('Error: Single diff failed'));
|
|
234
|
+
assert.ok(result.content[0].text.includes('Content mismatch'));
|
|
235
|
+
assert.ok(result.content[0].text.includes('Expected content:'));
|
|
236
|
+
assert.ok(result.content[0].text.includes('Actual content:'));
|
|
237
|
+
return true;
|
|
238
|
+
},
|
|
239
|
+
'should throw an error for content mismatch'
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should return an error for partial but not exact match', async () => {
|
|
244
|
+
const args = {
|
|
245
|
+
path: testFile1,
|
|
246
|
+
search_content: 'console.log("Hello World!");}', // Extra exclamation mark and closing brace
|
|
247
|
+
replace_content: 'new content',
|
|
248
|
+
start_line: 2
|
|
249
|
+
};
|
|
250
|
+
await assert.rejects(
|
|
251
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
252
|
+
(error) => {
|
|
253
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
254
|
+
assert.strictEqual(result.isError, true);
|
|
255
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
256
|
+
assert.ok(result.content[0].text.includes('Error: Single diff failed'));
|
|
257
|
+
assert.ok(result.content[0].text.includes('Content mismatch'));
|
|
258
|
+
return true;
|
|
259
|
+
},
|
|
260
|
+
'should throw an error for partial but not exact match'
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('Exact Matching', () => {
|
|
267
|
+
|
|
268
|
+
it('should require exact match including whitespace and indentation', async () => {
|
|
269
|
+
const args = {
|
|
270
|
+
path: testFile1,
|
|
271
|
+
search_content: 'console.log("Hello World");', // Missing leading spaces
|
|
272
|
+
replace_content: 'console.log("Hello Universe");',
|
|
273
|
+
start_line: 2
|
|
274
|
+
};
|
|
275
|
+
await assert.rejects(
|
|
276
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
277
|
+
(error) => {
|
|
278
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
279
|
+
assert.strictEqual(result.isError, true);
|
|
280
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
281
|
+
assert.ok(result.content[0].text.includes('Error: Single diff failed'));
|
|
282
|
+
assert.ok(result.content[0].text.includes('Content mismatch'));
|
|
283
|
+
return true;
|
|
284
|
+
},
|
|
285
|
+
'should throw an error for non-exact match'
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should successfully match content with correct indentation', async () => {
|
|
290
|
+
const args = {
|
|
291
|
+
path: testFile1,
|
|
292
|
+
search_content: ' console.log("Hello World");', // with correct indentation
|
|
293
|
+
replace_content: ' console.log("Hello Universe");',
|
|
294
|
+
start_line: 2
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
298
|
+
|
|
299
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
300
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should be case-sensitive', async () => {
|
|
304
|
+
const args = {
|
|
305
|
+
path: testFile1,
|
|
306
|
+
search_content: ' Console.log("Hello World");', // Capital 'C' in Console
|
|
307
|
+
replace_content: 'new content',
|
|
308
|
+
start_line: 2
|
|
309
|
+
};
|
|
310
|
+
await assert.rejects(
|
|
311
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
312
|
+
(error) => {
|
|
313
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
314
|
+
assert.strictEqual(result.isError, true);
|
|
315
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
316
|
+
assert.ok(result.content[0].text.includes('Error: Single diff failed'));
|
|
317
|
+
assert.ok(result.content[0].text.includes('Content mismatch'));
|
|
318
|
+
return true;
|
|
319
|
+
},
|
|
320
|
+
'should throw an error for case mismatch'
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Batch Operations and New Error Formatting', () => {
|
|
327
|
+
|
|
328
|
+
it('should return detailed error info in atomic mode', async () => {
|
|
329
|
+
const args = {
|
|
330
|
+
path: testFile1,
|
|
331
|
+
search_content: [
|
|
332
|
+
' console.log("Hello World");',
|
|
333
|
+
'this does not exist',
|
|
334
|
+
' }'
|
|
335
|
+
],
|
|
336
|
+
replace_content: [
|
|
337
|
+
' console.log("Hello Universe");',
|
|
338
|
+
'replacement content',
|
|
339
|
+
' this.updated = true;\n }'
|
|
340
|
+
],
|
|
341
|
+
start_line: [2, 3, 9],
|
|
342
|
+
atomic: true
|
|
343
|
+
};
|
|
344
|
+
await assert.rejects(
|
|
345
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
346
|
+
(error) => {
|
|
347
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
348
|
+
assert.strictEqual(result.isError, true);
|
|
349
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
350
|
+
assert.ok(result.content[0].text.includes('Error: Atomic operation failed'));
|
|
351
|
+
assert.ok(result.content[0].text.includes('Detailed results:'));
|
|
352
|
+
assert.ok(result.content[0].text.includes('Diff 1:'));
|
|
353
|
+
assert.ok(result.content[0].text.includes('Diff 2:'));
|
|
354
|
+
assert.ok(result.content[0].text.includes('Diff 3:'));
|
|
355
|
+
assert.ok(result.content[0].text.includes('Status: fail'));
|
|
356
|
+
assert.ok(result.content[0].text.includes('Status: aborted'));
|
|
357
|
+
return true;
|
|
358
|
+
},
|
|
359
|
+
'should throw detailed error on failure in atomic mode'
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should provide detailed partial success results in non-atomic mode', async () => {
|
|
364
|
+
const args = {
|
|
365
|
+
path: testFile1,
|
|
366
|
+
search_content: [
|
|
367
|
+
' console.log("Hello World");',
|
|
368
|
+
'this does not exist',
|
|
369
|
+
' }'
|
|
370
|
+
],
|
|
371
|
+
replace_content: [
|
|
372
|
+
' console.log("Hello Universe");',
|
|
373
|
+
'replacement content',
|
|
374
|
+
' this.updated = true;\n }'
|
|
375
|
+
],
|
|
376
|
+
start_line: [2, 3, 9],
|
|
377
|
+
atomic: false
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
381
|
+
|
|
382
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
383
|
+
assert.ok(result.content[0].text.includes('Batch diff operation (non-atomic) completed'));
|
|
384
|
+
assert.ok(result.content[0].text.includes('2/3 diffs applied successfully'));
|
|
385
|
+
assert.ok(result.content[0].text.includes('(1 failed)'));
|
|
386
|
+
assert.ok(result.content[0].text.includes('Detailed results:'));
|
|
387
|
+
assert.ok(result.content[0].text.includes('Status: success'));
|
|
388
|
+
assert.ok(result.content[0].text.includes('Status: fail'));
|
|
389
|
+
// check for diff results
|
|
390
|
+
assert.ok(result.content[0].text.includes('Diff 1:')); // Check for results info
|
|
391
|
+
assert.ok(result.content[0].text.includes('Diff 2:'));
|
|
392
|
+
assert.ok(result.content[0].text.includes('Diff 3:'));
|
|
393
|
+
|
|
394
|
+
// Verify partial file content update
|
|
395
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
396
|
+
assert.ok(content.includes('console.log("Hello Universe");'));
|
|
397
|
+
assert.ok(content.includes('this.updated = true;'));
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should provide concise error for single diff failure', async () => {
|
|
401
|
+
const args = {
|
|
402
|
+
path: testFile1,
|
|
403
|
+
search_content: 'nonexistent content',
|
|
404
|
+
replace_content: 'new content',
|
|
405
|
+
start_line: 2
|
|
406
|
+
};
|
|
407
|
+
await assert.rejects(
|
|
408
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
409
|
+
(error) => {
|
|
410
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
411
|
+
assert.strictEqual(result.isError, true);
|
|
412
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
413
|
+
assert.ok(result.content[0].text.includes('Error: Single diff failed:'));
|
|
414
|
+
assert.ok(result.content[0].text.includes('Content mismatch'));
|
|
415
|
+
assert.ok(result.content[0].text.includes('Expected content:'));
|
|
416
|
+
assert.ok(result.content[0].text.includes('Actual content:'));
|
|
417
|
+
assert.ok(result.content[0].text.includes('2 |'));
|
|
418
|
+
return true;
|
|
419
|
+
},
|
|
420
|
+
'should throw concise error for single diff failure'
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('should display formatted line numbers and content', async () => {
|
|
425
|
+
const args = {
|
|
426
|
+
path: testFile1,
|
|
427
|
+
search_content: 'wrong content at line 2',
|
|
428
|
+
replace_content: 'new content',
|
|
429
|
+
start_line: 2
|
|
430
|
+
};
|
|
431
|
+
await assert.rejects(
|
|
432
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
433
|
+
(error) => {
|
|
434
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
435
|
+
assert.strictEqual(result.isError, true);
|
|
436
|
+
const errorText = result.content[0].text;
|
|
437
|
+
assert.ok(errorText.includes('2 |'));
|
|
438
|
+
assert.ok(errorText.includes('Expected content:'));
|
|
439
|
+
assert.ok(errorText.includes('Actual content:'));
|
|
440
|
+
assert.ok(errorText.includes('This is diff #1 in the batch'));
|
|
441
|
+
return true;
|
|
442
|
+
},
|
|
443
|
+
'should throw on formatted line number and content error'
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('Edge Cases', () => {
|
|
450
|
+
|
|
451
|
+
it('should handle single-line files', async () => {
|
|
452
|
+
const singleLineFile = join(testDir, 'single.txt');
|
|
453
|
+
await fs.writeFile(singleLineFile, 'single line content', 'utf8');
|
|
454
|
+
|
|
455
|
+
const args = {
|
|
456
|
+
path: singleLineFile,
|
|
457
|
+
search_content: 'single line content',
|
|
458
|
+
replace_content: 'replaced single line',
|
|
459
|
+
start_line: 1
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
463
|
+
|
|
464
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
465
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
466
|
+
|
|
467
|
+
// Verify content
|
|
468
|
+
const content = await fs.readFile(singleLineFile, 'utf8');
|
|
469
|
+
assert.strictEqual(content, 'replaced single line');
|
|
470
|
+
|
|
471
|
+
await fs.unlink(singleLineFile);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should handle empty line replacement', async () => {
|
|
475
|
+
const fileWithEmptyLines = join(testDir, 'empty_lines.txt');
|
|
476
|
+
const contentWithEmpty = 'Line 1\n\nLine 3\n';
|
|
477
|
+
await fs.writeFile(fileWithEmptyLines, contentWithEmpty, 'utf8');
|
|
478
|
+
|
|
479
|
+
const args = {
|
|
480
|
+
path: fileWithEmptyLines,
|
|
481
|
+
search_content: '', // empty line
|
|
482
|
+
replace_content: 'Now not empty',
|
|
483
|
+
start_line: 2
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
487
|
+
|
|
488
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
489
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
490
|
+
|
|
491
|
+
// Verify content
|
|
492
|
+
const content = await fs.readFile(fileWithEmptyLines, 'utf8');
|
|
493
|
+
const lines = content.split('\n');
|
|
494
|
+
assert.strictEqual(lines[1], 'Now not empty');
|
|
495
|
+
|
|
496
|
+
await fs.unlink(fileWithEmptyLines);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should handle content with special characters', async () => {
|
|
500
|
+
const specialContent = `function special() {
|
|
501
|
+
const regex = /[áéíóú\\s@#$%^&*()]/g;
|
|
502
|
+
console.log("Special: 中文测试");
|
|
503
|
+
return regex.test("test");
|
|
504
|
+
}`;
|
|
505
|
+
|
|
506
|
+
const specialFile = join(testDir, 'special.txt');
|
|
507
|
+
await fs.writeFile(specialFile, specialContent, 'utf8');
|
|
508
|
+
|
|
509
|
+
const args = {
|
|
510
|
+
path: specialFile,
|
|
511
|
+
search_content: ' const regex = /[áéíóú\\s@#$%^&*()]/g;',
|
|
512
|
+
replace_content: ' const regex = /[a-zA-Z0-9]/g;',
|
|
513
|
+
start_line: 2
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
517
|
+
|
|
518
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
519
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
520
|
+
|
|
521
|
+
// Verify special characters are handled correctly
|
|
522
|
+
const content = await fs.readFile(specialFile, 'utf8');
|
|
523
|
+
assert.ok(content.includes('/[a-zA-Z0-9]/g;'));
|
|
524
|
+
assert.ok(!content.includes('/[áéíóú\\s@#$%^&*()]/g;'));
|
|
525
|
+
|
|
526
|
+
await fs.unlink(specialFile);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('should handle replacement at the end of the file', async () => {
|
|
530
|
+
const args = {
|
|
531
|
+
path: testFile1,
|
|
532
|
+
search_content: ' }',
|
|
533
|
+
replace_content: ' this.created = new Date();\n }',
|
|
534
|
+
start_line: 9 // last line
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
538
|
+
|
|
539
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
540
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
541
|
+
assert.ok(result.content[0].text.includes('added 1 line(s)'));
|
|
542
|
+
|
|
543
|
+
// Verify content
|
|
544
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
545
|
+
assert.ok(content.includes('this.created = new Date();'));
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe('Trim Option Functionality', () => {
|
|
551
|
+
|
|
552
|
+
it('should successfully match content with trim enabled when whitespace differs', async () => {
|
|
553
|
+
const args = {
|
|
554
|
+
path: testFile1,
|
|
555
|
+
search_content: 'console.log("Hello World");', // Missing leading spaces
|
|
556
|
+
replace_content: ' console.log("Hello Universe");', // With correct indentation in replacement
|
|
557
|
+
start_line: 2,
|
|
558
|
+
trim: true
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
562
|
+
|
|
563
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
564
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
565
|
+
|
|
566
|
+
// Verify replacement content is inserted exactly as provided
|
|
567
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
568
|
+
assert.ok(content.includes('console.log("Hello Universe");'));
|
|
569
|
+
assert.ok(!content.includes('console.log("Hello World");'));
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('should handle multi-line content with trim option', async () => {
|
|
573
|
+
const searchContent = `class TestClass {
|
|
574
|
+
constructor() {
|
|
575
|
+
this.value = 42;
|
|
576
|
+
}
|
|
577
|
+
}`; // No proper indentation
|
|
578
|
+
const replaceContent = `class TestClass {
|
|
579
|
+
constructor(value = 0) {
|
|
580
|
+
this.value = value;
|
|
581
|
+
this.name = "test";
|
|
582
|
+
}
|
|
583
|
+
}`; // Proper indentation in replacement
|
|
584
|
+
|
|
585
|
+
const args = {
|
|
586
|
+
path: testFile1,
|
|
587
|
+
search_content: searchContent,
|
|
588
|
+
replace_content: replaceContent,
|
|
589
|
+
start_line: 6,
|
|
590
|
+
trim: true
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
594
|
+
|
|
595
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
596
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
597
|
+
|
|
598
|
+
// Verify replacement content maintains its indentation
|
|
599
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
600
|
+
assert.ok(content.includes(' constructor(value = 0) {'));
|
|
601
|
+
assert.ok(content.includes(' this.name = "test";'));
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should fail when content does not match even with trim enabled', async () => {
|
|
605
|
+
const args = {
|
|
606
|
+
path: testFile1,
|
|
607
|
+
search_content: 'completely different content',
|
|
608
|
+
replace_content: 'new content',
|
|
609
|
+
start_line: 2,
|
|
610
|
+
trim: true
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
await assert.rejects(
|
|
614
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
615
|
+
(error) => {
|
|
616
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
617
|
+
assert.strictEqual(result.isError, true);
|
|
618
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
619
|
+
assert.ok(result.content[0].text.includes('Error: Single diff failed'));
|
|
620
|
+
assert.ok(result.content[0].text.includes('Content mismatch'));
|
|
621
|
+
return true;
|
|
622
|
+
},
|
|
623
|
+
'should throw an error when content does not match even with trim'
|
|
624
|
+
);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('should display original untrimmed content in error messages', async () => {
|
|
628
|
+
const args = {
|
|
629
|
+
path: testFile1,
|
|
630
|
+
search_content: 'wrong content',
|
|
631
|
+
replace_content: 'new content',
|
|
632
|
+
start_line: 2,
|
|
633
|
+
trim: true
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
await assert.rejects(
|
|
637
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
638
|
+
(error) => {
|
|
639
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
640
|
+
assert.strictEqual(result.isError, true);
|
|
641
|
+
const errorText = result.content[0].text;
|
|
642
|
+
|
|
643
|
+
// Error message should show original content with whitespace
|
|
644
|
+
assert.ok(errorText.includes('Expected content:'));
|
|
645
|
+
assert.ok(errorText.includes('Actual content:'));
|
|
646
|
+
assert.ok(errorText.includes('2 | console.log("Hello World");')); // Original with spaces
|
|
647
|
+
return true;
|
|
648
|
+
},
|
|
649
|
+
'should show original untrimmed content in error messages'
|
|
650
|
+
);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('should work with trailing whitespace differences when trim is enabled', async () => {
|
|
654
|
+
// Create a test file with trailing spaces
|
|
655
|
+
const contentWithTrailing = `function example() {
|
|
656
|
+
console.log("Hello World");
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
class TestClass {
|
|
661
|
+
constructor() {
|
|
662
|
+
this.value = 42;
|
|
663
|
+
}
|
|
664
|
+
}`;
|
|
665
|
+
const trailingSpaceFile = join(testDir, 'trailing.txt');
|
|
666
|
+
await fs.writeFile(trailingSpaceFile, contentWithTrailing, 'utf8');
|
|
667
|
+
|
|
668
|
+
const args = {
|
|
669
|
+
path: trailingSpaceFile,
|
|
670
|
+
search_content: ' console.log("Hello World");', // No trailing spaces
|
|
671
|
+
replace_content: ' console.log("Hello Universe");',
|
|
672
|
+
start_line: 2,
|
|
673
|
+
trim: true
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
677
|
+
|
|
678
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
679
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
680
|
+
|
|
681
|
+
await fs.unlink(trailingSpaceFile);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should maintain exact replacement content formatting regardless of trim setting', async () => {
|
|
685
|
+
const args = {
|
|
686
|
+
path: testFile1,
|
|
687
|
+
search_content: 'console.log("Hello World");', // No leading spaces
|
|
688
|
+
replace_content: ' console.log("Hello Universe"); ', // Extra indentation and trailing spaces
|
|
689
|
+
start_line: 2,
|
|
690
|
+
trim: true
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
694
|
+
|
|
695
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
696
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
697
|
+
|
|
698
|
+
// Verify replacement content is inserted exactly as provided, including extra spaces
|
|
699
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
700
|
+
const lines = content.split('\n');
|
|
701
|
+
assert.ok(lines[1].includes(' console.log("Hello Universe"); '));
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('should default to trim: false when not specified', async () => {
|
|
705
|
+
// This should fail because trim defaults to false and spaces don't match
|
|
706
|
+
const args = {
|
|
707
|
+
path: testFile1,
|
|
708
|
+
search_content: 'console.log("Hello World");', // Missing leading spaces
|
|
709
|
+
replace_content: 'console.log("Hello Universe");',
|
|
710
|
+
start_line: 2
|
|
711
|
+
// trim not specified, should default to false
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
await assert.rejects(
|
|
715
|
+
async () => await ApplyDiffHandler.handle(args),
|
|
716
|
+
(error) => {
|
|
717
|
+
const result = FileUtils.createErrorResponse(error.message);
|
|
718
|
+
assert.strictEqual(result.isError, true);
|
|
719
|
+
assert.ok(result.content[0].text.includes('Content mismatch'));
|
|
720
|
+
return true;
|
|
721
|
+
},
|
|
722
|
+
'should fail with default trim: false when whitespace does not match'
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('should handle empty lines correctly with trim option', async () => {
|
|
727
|
+
const fileWithEmptyLines = join(testDir, 'empty_trim.txt');
|
|
728
|
+
const contentWithEmpty = 'Line 1\n \nLine 3\n'; // Middle line has spaces
|
|
729
|
+
await fs.writeFile(fileWithEmptyLines, contentWithEmpty, 'utf8');
|
|
730
|
+
|
|
731
|
+
const args = {
|
|
732
|
+
path: fileWithEmptyLines,
|
|
733
|
+
search_content: '', // Empty string should match trimmed spaces
|
|
734
|
+
replace_content: 'Not empty anymore',
|
|
735
|
+
start_line: 2,
|
|
736
|
+
trim: true
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const result = await ApplyDiffHandler.handle(args);
|
|
740
|
+
|
|
741
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
742
|
+
assert.ok(result.content[0].text.includes('Successfully applied diff'));
|
|
743
|
+
|
|
744
|
+
// Verify content
|
|
745
|
+
const content = await fs.readFile(fileWithEmptyLines, 'utf8');
|
|
746
|
+
const lines = content.split('\n');
|
|
747
|
+
assert.strictEqual(lines[1], 'Not empty anymore');
|
|
748
|
+
|
|
749
|
+
await fs.unlink(fileWithEmptyLines);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
});
|