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,505 @@
|
|
|
1
|
+
import { SearchAndReplaceHandler } from '../src/handlers/searchAndReplace.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
|
+
* SearchAndReplaceHandler 测试类
|
|
11
|
+
* 测试搜索替换功能
|
|
12
|
+
*/
|
|
13
|
+
describe('SearchAndReplaceHandler', () => {
|
|
14
|
+
|
|
15
|
+
const testDir = './test_files_search';
|
|
16
|
+
const testFile1 = join(testDir, 'test1.txt');
|
|
17
|
+
const baseContent = `Hello World
|
|
18
|
+
This is a test file
|
|
19
|
+
Hello again
|
|
20
|
+
HELLO in uppercase
|
|
21
|
+
hello in lowercase
|
|
22
|
+
Test pattern: abc123
|
|
23
|
+
Another test: ABC456
|
|
24
|
+
End of file`;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
// 设置测试工作区
|
|
28
|
+
FileUtils.setWorkspaceRoot(process.cwd());
|
|
29
|
+
|
|
30
|
+
// 创建测试目录和文件
|
|
31
|
+
if (!existsSync(testDir)) {
|
|
32
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
await fs.writeFile(testFile1, baseContent, 'utf8');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
// 清理测试文件
|
|
39
|
+
try {
|
|
40
|
+
if (existsSync(testFile1)) await fs.unlink(testFile1);
|
|
41
|
+
if (existsSync(testDir)) await fs.rmdir(testDir);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn('清理测试文件失败:', error.message);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('基本字符串替换', () => {
|
|
48
|
+
|
|
49
|
+
it('应该成功替换简单字符串', async () => {
|
|
50
|
+
const args = {
|
|
51
|
+
path: testFile1,
|
|
52
|
+
search: 'Hello',
|
|
53
|
+
replace: 'Hi'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
57
|
+
|
|
58
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
59
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 2 occurrence(s)'));
|
|
60
|
+
|
|
61
|
+
// 验证文件内容
|
|
62
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
63
|
+
assert.ok(content.includes('Hi World'));
|
|
64
|
+
assert.ok(content.includes('Hi again'));
|
|
65
|
+
assert.ok(!content.includes('Hello World'));
|
|
66
|
+
assert.ok(!content.includes('Hello again'));
|
|
67
|
+
// HELLO 和 hello 应该保持不变(大小写敏感)
|
|
68
|
+
assert.ok(content.includes('HELLO in uppercase'));
|
|
69
|
+
assert.ok(content.includes('hello in lowercase'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('应该处理没有匹配的情况', async () => {
|
|
73
|
+
const args = {
|
|
74
|
+
path: testFile1,
|
|
75
|
+
search: 'nonexistent',
|
|
76
|
+
replace: 'replacement'
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
80
|
+
|
|
81
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
82
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 0 occurrence(s)'));
|
|
83
|
+
|
|
84
|
+
// 文件内容应该保持不变
|
|
85
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
86
|
+
assert.strictEqual(content, baseContent);
|
|
87
|
+
}); it('应该替换所有匹配的实例', async () => {
|
|
88
|
+
const args = {
|
|
89
|
+
path: testFile1,
|
|
90
|
+
search: 'test',
|
|
91
|
+
replace: 'exam'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
95
|
+
|
|
96
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
97
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 2 occurrence(s)'));
|
|
98
|
+
|
|
99
|
+
// 验证所有 "test" 都被替换
|
|
100
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
101
|
+
assert.ok(content.includes('This is a exam file'));
|
|
102
|
+
assert.ok(content.includes('Test pattern')); // "Test" 不应该被替换(大写T)
|
|
103
|
+
assert.ok(content.includes('Another exam'));
|
|
104
|
+
assert.ok(!content.includes('test')); // 小写的test应该都被替换
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('大小写不敏感替换', () => {
|
|
110
|
+
|
|
111
|
+
it('应该进行大小写不敏感的替换', async () => {
|
|
112
|
+
const args = {
|
|
113
|
+
path: testFile1,
|
|
114
|
+
search: 'hello',
|
|
115
|
+
replace: 'greetings',
|
|
116
|
+
ignore_case: true
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
120
|
+
|
|
121
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
122
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 4 occurrence(s)'));
|
|
123
|
+
assert.ok(result.content[0].text.includes('(case-insensitive)'));
|
|
124
|
+
|
|
125
|
+
// 验证所有变体的 hello 都被替换
|
|
126
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
127
|
+
assert.ok(content.includes('greetings World'));
|
|
128
|
+
assert.ok(content.includes('greetings again'));
|
|
129
|
+
assert.ok(content.includes('greetings in uppercase'));
|
|
130
|
+
assert.ok(content.includes('greetings in lowercase'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('应该保持原始文本的大小写结构', async () => {
|
|
134
|
+
const args = {
|
|
135
|
+
path: testFile1,
|
|
136
|
+
search: 'test',
|
|
137
|
+
replace: 'EXAM',
|
|
138
|
+
ignore_case: true
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
142
|
+
|
|
143
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
144
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 3 occurrence(s)'));
|
|
145
|
+
|
|
146
|
+
// 验证替换但保持原来的位置
|
|
147
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
148
|
+
assert.ok(content.includes('This is a EXAM file'));
|
|
149
|
+
assert.ok(content.includes('EXAM pattern'));
|
|
150
|
+
assert.ok(content.includes('Another EXAM'));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('正则表达式替换', () => {
|
|
156
|
+
|
|
157
|
+
it('应该支持简单正则表达式', async () => {
|
|
158
|
+
const args = {
|
|
159
|
+
path: testFile1,
|
|
160
|
+
search: '\\d+',
|
|
161
|
+
replace: 'NUMBER',
|
|
162
|
+
use_regex: true
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
166
|
+
|
|
167
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
168
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 2 occurrence(s)'));
|
|
169
|
+
assert.ok(result.content[0].text.includes('using regex pattern'));
|
|
170
|
+
|
|
171
|
+
// 验证数字被替换
|
|
172
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
173
|
+
assert.ok(content.includes('abcNUMBER'));
|
|
174
|
+
assert.ok(content.includes('ABCNUMBER'));
|
|
175
|
+
assert.ok(!content.includes('123'));
|
|
176
|
+
assert.ok(!content.includes('456'));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('应该支持复杂正则表达式', async () => {
|
|
180
|
+
const args = {
|
|
181
|
+
path: testFile1,
|
|
182
|
+
search: '[A-Z]{3}\\d{3}',
|
|
183
|
+
replace: 'CODE',
|
|
184
|
+
use_regex: true
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
188
|
+
|
|
189
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
190
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 1 occurrence(s)'));
|
|
191
|
+
|
|
192
|
+
// 验证只有 ABC456 被替换
|
|
193
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
194
|
+
assert.ok(content.includes('Another test: CODE'));
|
|
195
|
+
assert.ok(content.includes('abc123')); // 这个不应该被替换
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('应该支持正则表达式捕获组', async () => {
|
|
199
|
+
const args = {
|
|
200
|
+
path: testFile1,
|
|
201
|
+
search: '([a-z]+)(\\d+)',
|
|
202
|
+
replace: '$2-$1',
|
|
203
|
+
use_regex: true
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
207
|
+
|
|
208
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
209
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 1 occurrence(s)'));
|
|
210
|
+
|
|
211
|
+
// 验证捕获组替换
|
|
212
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
213
|
+
assert.ok(content.includes('Test pattern: 123-abc'));
|
|
214
|
+
assert.ok(!content.includes('abc123'));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('应该结合正则表达式和大小写不敏感', async () => {
|
|
218
|
+
const args = {
|
|
219
|
+
path: testFile1,
|
|
220
|
+
search: 'hello',
|
|
221
|
+
replace: 'GREETINGS',
|
|
222
|
+
use_regex: true,
|
|
223
|
+
ignore_case: true
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
227
|
+
|
|
228
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
229
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 4 occurrence(s)'));
|
|
230
|
+
assert.ok(result.content[0].text.includes('using regex pattern'));
|
|
231
|
+
assert.ok(result.content[0].text.includes('(case-insensitive)'));
|
|
232
|
+
|
|
233
|
+
// 验证所有 hello 变体被替换
|
|
234
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
235
|
+
assert.ok(content.includes('GREETINGS World'));
|
|
236
|
+
assert.ok(content.includes('GREETINGS again'));
|
|
237
|
+
assert.ok(content.includes('GREETINGS in uppercase'));
|
|
238
|
+
assert.ok(content.includes('GREETINGS in lowercase'));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('应该抛出错误当正则表达式无效时', async () => {
|
|
242
|
+
const args = {
|
|
243
|
+
path: testFile1,
|
|
244
|
+
search: '[invalid regex',
|
|
245
|
+
replace: 'replacement',
|
|
246
|
+
use_regex: true
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
await assert.rejects(
|
|
250
|
+
() => SearchAndReplaceHandler.handle(args),
|
|
251
|
+
(error) => error.message.includes('Invalid regular expression')
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('行范围限制', () => {
|
|
258
|
+
it('应该只在指定起始行之后替换', async () => {
|
|
259
|
+
const args = {
|
|
260
|
+
path: testFile1,
|
|
261
|
+
search: 'test',
|
|
262
|
+
replace: 'exam',
|
|
263
|
+
start_line: 5
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
267
|
+
|
|
268
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
269
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 1 occurrence(s)'));
|
|
270
|
+
assert.ok(result.content[0].text.includes('(from line 5)'));
|
|
271
|
+
|
|
272
|
+
// 验证只有第5行之后的test被替换
|
|
273
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
274
|
+
assert.ok(content.includes('This is a test file')); // 第2行,不应该被替换
|
|
275
|
+
assert.ok(content.includes('Test pattern')); // 第6行,"Test"不会被替换(大写T)
|
|
276
|
+
assert.ok(content.includes('Another exam')); // 第7行,应该被替换
|
|
277
|
+
assert.ok(content.includes('Another exam')); // 第7行,应该被替换
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('应该只在指定结束行之前替换', async () => {
|
|
281
|
+
const args = {
|
|
282
|
+
path: testFile1,
|
|
283
|
+
search: 'test',
|
|
284
|
+
replace: 'exam',
|
|
285
|
+
end_line: 3
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
289
|
+
|
|
290
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
291
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 1 occurrence(s)'));
|
|
292
|
+
assert.ok(result.content[0].text.includes('(up to line 3)'));
|
|
293
|
+
|
|
294
|
+
// 验证只有前3行的test被替换
|
|
295
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
296
|
+
assert.ok(content.includes('This is a exam file')); // 第2行,应该被替换
|
|
297
|
+
assert.ok(content.includes('Test pattern')); // 第6行,不应该被替换
|
|
298
|
+
assert.ok(content.includes('Another test')); // 第7行,不应该被替换
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('应该在指定行范围内替换', async () => {
|
|
302
|
+
const args = {
|
|
303
|
+
path: testFile1,
|
|
304
|
+
search: 'Hello',
|
|
305
|
+
replace: 'Hi',
|
|
306
|
+
start_line: 2,
|
|
307
|
+
end_line: 4
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
311
|
+
|
|
312
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
313
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 1 occurrence(s)'));
|
|
314
|
+
assert.ok(result.content[0].text.includes('(lines 2-4)'));
|
|
315
|
+
|
|
316
|
+
// 验证只有指定范围内的Hello被替换
|
|
317
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
318
|
+
assert.ok(content.includes('Hello World')); // 第1行,不应该被替换
|
|
319
|
+
assert.ok(content.includes('Hi again')); // 第3行,应该被替换
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('应该抛出错误当起始行号无效时', async () => {
|
|
323
|
+
const args = {
|
|
324
|
+
path: testFile1,
|
|
325
|
+
search: 'test',
|
|
326
|
+
replace: 'exam',
|
|
327
|
+
start_line: 0
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
await assert.rejects(
|
|
331
|
+
() => SearchAndReplaceHandler.handle(args),
|
|
332
|
+
(error) => error.message.includes('Invalid start_line')
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('应该抛出错误当结束行号无效时', async () => {
|
|
337
|
+
const args = {
|
|
338
|
+
path: testFile1,
|
|
339
|
+
search: 'test',
|
|
340
|
+
replace: 'exam',
|
|
341
|
+
end_line: 0
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
await assert.rejects(
|
|
345
|
+
() => SearchAndReplaceHandler.handle(args),
|
|
346
|
+
(error) => error.message.includes('Invalid end_line')
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('应该抛出错误当起始行大于结束行时', async () => {
|
|
351
|
+
const args = {
|
|
352
|
+
path: testFile1,
|
|
353
|
+
search: 'test',
|
|
354
|
+
replace: 'exam',
|
|
355
|
+
start_line: 5,
|
|
356
|
+
end_line: 3
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
await assert.rejects(
|
|
360
|
+
() => SearchAndReplaceHandler.handle(args),
|
|
361
|
+
(error) => error.message.includes('cannot be greater than end_line')
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('应该抛出错误当起始行超出文件范围时', async () => {
|
|
366
|
+
const args = {
|
|
367
|
+
path: testFile1,
|
|
368
|
+
search: 'test',
|
|
369
|
+
replace: 'exam',
|
|
370
|
+
start_line: 100
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
await assert.rejects(
|
|
374
|
+
() => SearchAndReplaceHandler.handle(args),
|
|
375
|
+
(error) => error.message.includes('exceeds file length')
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('错误处理', () => {
|
|
382
|
+
|
|
383
|
+
it('应该抛出错误当文件不存在时', async () => {
|
|
384
|
+
const args = {
|
|
385
|
+
path: './nonexistent.txt',
|
|
386
|
+
search: 'test',
|
|
387
|
+
replace: 'exam'
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
await assert.rejects(
|
|
391
|
+
() => SearchAndReplaceHandler.handle(args),
|
|
392
|
+
(error) => error.message.includes('File not found')
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('边界情况', () => {
|
|
399
|
+
|
|
400
|
+
it('应该处理空搜索字符串', async () => {
|
|
401
|
+
const args = {
|
|
402
|
+
path: testFile1,
|
|
403
|
+
search: '',
|
|
404
|
+
replace: 'X'
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
408
|
+
|
|
409
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
410
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 0 occurrence(s)'));
|
|
411
|
+
|
|
412
|
+
// 文件应该保持不变
|
|
413
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
414
|
+
assert.strictEqual(content, baseContent);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('应该处理空替换字符串(删除)', async () => {
|
|
418
|
+
const args = {
|
|
419
|
+
path: testFile1,
|
|
420
|
+
search: 'Hello ',
|
|
421
|
+
replace: ''
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
425
|
+
|
|
426
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
427
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 2 occurrence(s)'));
|
|
428
|
+
|
|
429
|
+
// 验证 "Hello " 被删除
|
|
430
|
+
const content = await fs.readFile(testFile1, 'utf8');
|
|
431
|
+
assert.ok(content.includes('World\n')); // "Hello " 被删除,只剩 "World"
|
|
432
|
+
assert.ok(content.includes('again\n')); // "Hello " 被删除,只剩 "again"
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('应该处理包含特殊字符的搜索和替换', async () => {
|
|
436
|
+
const specialFile = join(testDir, 'special.txt');
|
|
437
|
+
const specialContent = 'Price: $100\nRegex: /test/g\nEmail: test@example.com';
|
|
438
|
+
await fs.writeFile(specialFile, specialContent, 'utf8');
|
|
439
|
+
|
|
440
|
+
const args = {
|
|
441
|
+
path: specialFile,
|
|
442
|
+
search: '$100',
|
|
443
|
+
replace: '€85'
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
447
|
+
|
|
448
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
449
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 1 occurrence(s)'));
|
|
450
|
+
|
|
451
|
+
// 验证特殊字符被正确处理
|
|
452
|
+
const content = await fs.readFile(specialFile, 'utf8');
|
|
453
|
+
assert.ok(content.includes('Price: €85'));
|
|
454
|
+
assert.ok(!content.includes('$100'));
|
|
455
|
+
|
|
456
|
+
await fs.unlink(specialFile);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('应该处理单行文件', async () => {
|
|
460
|
+
const singleLineFile = join(testDir, 'single.txt');
|
|
461
|
+
await fs.writeFile(singleLineFile, 'single line with test content', 'utf8');
|
|
462
|
+
|
|
463
|
+
const args = {
|
|
464
|
+
path: singleLineFile,
|
|
465
|
+
search: 'test',
|
|
466
|
+
replace: 'exam'
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
470
|
+
|
|
471
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
472
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 1 occurrence(s)'));
|
|
473
|
+
|
|
474
|
+
// 验证替换
|
|
475
|
+
const content = await fs.readFile(singleLineFile, 'utf8');
|
|
476
|
+
assert.strictEqual(content, 'single line with exam content');
|
|
477
|
+
|
|
478
|
+
await fs.unlink(singleLineFile);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('应该处理空文件', async () => {
|
|
482
|
+
const emptyFile = join(testDir, 'empty.txt');
|
|
483
|
+
await fs.writeFile(emptyFile, '', 'utf8');
|
|
484
|
+
|
|
485
|
+
const args = {
|
|
486
|
+
path: emptyFile,
|
|
487
|
+
search: 'anything',
|
|
488
|
+
replace: 'something'
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const result = await SearchAndReplaceHandler.handle(args);
|
|
492
|
+
|
|
493
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
494
|
+
assert.ok(result.content[0].text.includes('Successfully replaced 0 occurrence(s)'));
|
|
495
|
+
|
|
496
|
+
// 文件应该保持为空
|
|
497
|
+
const content = await fs.readFile(emptyFile, 'utf8');
|
|
498
|
+
assert.strictEqual(content, '');
|
|
499
|
+
|
|
500
|
+
await fs.unlink(emptyFile);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
});
|