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,290 @@
|
|
|
1
|
+
import { SetWorkspaceHandler } from '../src/handlers/setWorkspace.js';
|
|
2
|
+
import { FileUtils } from '../src/utils/fileUtils.js';
|
|
3
|
+
import { describe, it, beforeEach, after } from 'node:test';
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Test suite for SetWorkspaceHandler.
|
|
11
|
+
* Tests the workspace setting functionality.
|
|
12
|
+
*/
|
|
13
|
+
describe('SetWorkspaceHandler', () => {
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset workspace state
|
|
17
|
+
FileUtils.setWorkspaceRoot(process.cwd());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('handle method', () => {
|
|
21
|
+
it('should successfully set the workspace root directory', async () => {
|
|
22
|
+
const testPath = process.cwd(); // Use the current directory, ensure it exists
|
|
23
|
+
const args = { path: testPath };
|
|
24
|
+
|
|
25
|
+
const result = await SetWorkspaceHandler.handle(args);
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
28
|
+
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
29
|
+
assert.ok(result.content[0].text.includes(testPath));
|
|
30
|
+
assert.strictEqual(FileUtils.getWorkspaceRoot(), testPath);
|
|
31
|
+
}); it('should handle Windows path format', async () => {
|
|
32
|
+
if (process.platform !== 'win32') {
|
|
33
|
+
// Skip this test on non-Windows platforms
|
|
34
|
+
console.log('Skipping Windows path format test: not on a Windows platform');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const testPath = 'C:\\'; // Use the root directory, usually exists on Windows
|
|
38
|
+
const args = { path: testPath };
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = await SetWorkspaceHandler.handle(args);
|
|
42
|
+
|
|
43
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
44
|
+
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// If C:\ does not exist (very rare), it should throw a directory not found error
|
|
47
|
+
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
48
|
+
}
|
|
49
|
+
}); it('should handle relative paths', async () => {
|
|
50
|
+
const testPath = '.'; // Use the current directory
|
|
51
|
+
const args = { path: testPath };
|
|
52
|
+
|
|
53
|
+
const result = await SetWorkspaceHandler.handle(args);
|
|
54
|
+
|
|
55
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
56
|
+
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
57
|
+
// Relative paths are resolved to absolute paths
|
|
58
|
+
assert.ok(FileUtils.getWorkspaceRoot().includes(process.cwd()));
|
|
59
|
+
}); it('should throw an error when the directory does not exist', async () => {
|
|
60
|
+
const testPath = './nonexistent/directory';
|
|
61
|
+
const args = { path: testPath };
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await SetWorkspaceHandler.handle(args);
|
|
65
|
+
assert.fail('Should have thrown an error');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
68
|
+
}
|
|
69
|
+
}); it('should throw an error when the path contains a non-existent directory with special characters', async () => {
|
|
70
|
+
const testPath = '/path/with spaces/and-symbols_123';
|
|
71
|
+
const args = { path: testPath };
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await SetWorkspaceHandler.handle(args);
|
|
75
|
+
assert.fail('Should have thrown an error');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should throw an error when the path parameter is missing', async () => {
|
|
82
|
+
const args = {};
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await SetWorkspaceHandler.handle(args);
|
|
86
|
+
assert.fail('Should have thrown an error');
|
|
87
|
+
} catch (error) {
|
|
88
|
+
assert.ok(error.message.includes('Failed to set workspace'));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Edge cases', () => {
|
|
95
|
+
|
|
96
|
+
it('should handle null path', async () => {
|
|
97
|
+
const args = { path: null };
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await SetWorkspaceHandler.handle(args);
|
|
101
|
+
// If no error is thrown, check the result
|
|
102
|
+
assert.strictEqual(FileUtils.getWorkspaceRoot(), null);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// If an error is thrown, ensure it is a reasonable error message
|
|
105
|
+
assert.ok(error.message.includes('Failed to set workspace'));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle undefined path', async () => {
|
|
110
|
+
const args = { path: undefined };
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await SetWorkspaceHandler.handle(args);
|
|
114
|
+
assert.fail('Should have thrown an error');
|
|
115
|
+
} catch (error) {
|
|
116
|
+
assert.ok(error.message.includes('Failed to set workspace'));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Path security tests', () => {
|
|
123
|
+
let testWorkspace;
|
|
124
|
+
|
|
125
|
+
beforeEach(async () => {
|
|
126
|
+
testWorkspace = join(tmpdir(), `mcp-security-test-${Date.now()}`);
|
|
127
|
+
await fs.mkdir(testWorkspace, { recursive: true });
|
|
128
|
+
|
|
129
|
+
// Create test directory structure
|
|
130
|
+
await fs.mkdir(join(testWorkspace, 'safe'), { recursive: true });
|
|
131
|
+
await fs.mkdir(join(testWorkspace, 'safe', 'nested'), { recursive: true });
|
|
132
|
+
await fs.writeFile(join(testWorkspace, 'safe', 'test.txt'), 'safe content');
|
|
133
|
+
await fs.writeFile(join(testWorkspace, 'safe', 'nested', 'deep.txt'), 'deep content');
|
|
134
|
+
|
|
135
|
+
// Create a file outside the workspace
|
|
136
|
+
await fs.writeFile(join(tmpdir(), 'outside.txt'), 'outside content');
|
|
137
|
+
|
|
138
|
+
FileUtils.setWorkspaceRoot(testWorkspace);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
after(async () => {
|
|
142
|
+
try {
|
|
143
|
+
await fs.rm(testWorkspace, { recursive: true, force: true });
|
|
144
|
+
await fs.rm(join(tmpdir(), 'outside.txt'), { force: true });
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Ignore cleanup errors
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should succeed for normal path access', async () => {
|
|
151
|
+
// Test relative path
|
|
152
|
+
const safePath1 = FileUtils.getSecurePath('safe/test.txt');
|
|
153
|
+
assert.ok(safePath1.includes('test.txt'));
|
|
154
|
+
|
|
155
|
+
// Test nested path
|
|
156
|
+
const safePath2 = FileUtils.getSecurePath('safe/nested/deep.txt');
|
|
157
|
+
assert.ok(safePath2.includes('deep.txt'));
|
|
158
|
+
|
|
159
|
+
// Test current directory
|
|
160
|
+
const safePath3 = FileUtils.getSecurePath('.');
|
|
161
|
+
assert.strictEqual(safePath3, testWorkspace);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should prevent path traversal attacks', async () => {
|
|
165
|
+
const traversalAttempts = [
|
|
166
|
+
'../outside.txt', // Basic traversal
|
|
167
|
+
'../../outside.txt', // Deep traversal
|
|
168
|
+
'safe/../../../outside.txt', // Mixed traversal
|
|
169
|
+
'safe/../../outside.txt', // Nested traversal
|
|
170
|
+
'/etc/passwd', // Absolute path attack (Unix)
|
|
171
|
+
];
|
|
172
|
+
if (process.platform === 'win32') {
|
|
173
|
+
traversalAttempts.push(
|
|
174
|
+
'C:\\Windows\\System32\\config\\SAM', // Absolute path attack (Windows)
|
|
175
|
+
'..\\..\\outside.txt', // Windows-style traversal
|
|
176
|
+
'safe\\..\\..\\outside.txt', // Windows mixed traversal
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
for (const maliciousPath of traversalAttempts) {
|
|
180
|
+
await assert.rejects(
|
|
181
|
+
async () => FileUtils.getSecurePath(maliciousPath),
|
|
182
|
+
(error) => {
|
|
183
|
+
return error.message.includes('Access denied') ||
|
|
184
|
+
error.message.includes('outside the workspace');
|
|
185
|
+
},
|
|
186
|
+
`Path traversal attack should be blocked: ${maliciousPath}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should reject dangerous characters', async () => {
|
|
192
|
+
const dangerousInputs = [
|
|
193
|
+
'test\0file.txt', // Null byte
|
|
194
|
+
'test\x00file.txt', // Hex null byte
|
|
195
|
+
'', // Empty string
|
|
196
|
+
null, // Null value
|
|
197
|
+
undefined, // Undefined value
|
|
198
|
+
123, // Number type
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
for (const dangerousInput of dangerousInputs) {
|
|
202
|
+
await assert.rejects(
|
|
203
|
+
async () => FileUtils.getSecurePath(dangerousInput),
|
|
204
|
+
(error) => {
|
|
205
|
+
return error.message.includes('Invalid file path') ||
|
|
206
|
+
error.message.includes('null bytes');
|
|
207
|
+
},
|
|
208
|
+
`Dangerous input should be rejected: ${dangerousInput}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// it('should prevent symlink traversal', async () => {
|
|
214
|
+
// // Create a symlink pointing outside the workspace
|
|
215
|
+
// const symlinkPath = join(testWorkspace, 'malicious_link');
|
|
216
|
+
// const outsidePath = join(tmpdir(), 'outside.txt');
|
|
217
|
+
//
|
|
218
|
+
// try {
|
|
219
|
+
// await fs.symlink(outsidePath, symlinkPath);
|
|
220
|
+
//
|
|
221
|
+
// // Attempt to access the file outside the workspace via the symlink
|
|
222
|
+
// await assert.rejects(
|
|
223
|
+
// async () => FileUtils.getSecurePath('malicious_link'),
|
|
224
|
+
// (error) => error.message.includes('Access denied') ||
|
|
225
|
+
// error.message.includes('outside the workspace'),
|
|
226
|
+
// 'Symlink traversal attack should be blocked'
|
|
227
|
+
// );
|
|
228
|
+
// } catch (error) {
|
|
229
|
+
// // Some systems may not support creating symlinks, skip this test
|
|
230
|
+
// if (error.code === 'EPERM' || error.code === 'ENOENT') {
|
|
231
|
+
// console.log('Skipping symlink test: system does not support or permission denied');
|
|
232
|
+
// return;
|
|
233
|
+
// }
|
|
234
|
+
// throw error;
|
|
235
|
+
// }
|
|
236
|
+
// });
|
|
237
|
+
|
|
238
|
+
it('should work correctly for workspace validation', async () => {
|
|
239
|
+
// Test invalid workspace settings
|
|
240
|
+
await assert.rejects(
|
|
241
|
+
async () => FileUtils.setWorkspaceRoot(''),
|
|
242
|
+
(error) => error.message.includes('Invalid workspace root'),
|
|
243
|
+
'Empty workspace path should be rejected'
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await assert.rejects(
|
|
247
|
+
async () => FileUtils.setWorkspaceRoot(null),
|
|
248
|
+
(error) => error.message.includes('Invalid workspace root'),
|
|
249
|
+
'Null workspace path should be rejected'
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await assert.rejects(
|
|
253
|
+
async () => FileUtils.setWorkspaceRoot('/nonexistent/directory'),
|
|
254
|
+
(error) => error.message.includes('does not exist'),
|
|
255
|
+
'Non-existent directory should be rejected'
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Test file as workspace
|
|
259
|
+
const tempFile = join(tmpdir(), `test-file-${Date.now()}.txt`);
|
|
260
|
+
await fs.writeFile(tempFile, 'test');
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await assert.rejects(
|
|
264
|
+
async () => FileUtils.setWorkspaceRoot(tempFile),
|
|
265
|
+
(error) => error.message.includes('not a directory'),
|
|
266
|
+
'File path as workspace should be rejected'
|
|
267
|
+
);
|
|
268
|
+
} finally {
|
|
269
|
+
await fs.rm(tempFile, { force: true });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should deny access when workspace is not set', async () => {
|
|
274
|
+
// Temporarily clear workspace setting
|
|
275
|
+
const originalWorkspace = FileUtils.WORKSPACE_ROOT;
|
|
276
|
+
FileUtils.WORKSPACE_ROOT = null;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await assert.rejects(
|
|
280
|
+
async () => FileUtils.getSecurePath('test.txt'),
|
|
281
|
+
(error) => error.message.includes('Workspace not set'),
|
|
282
|
+
'Access should be denied when workspace is not set'
|
|
283
|
+
);
|
|
284
|
+
} finally {
|
|
285
|
+
// Restore workspace setting
|
|
286
|
+
FileUtils.WORKSPACE_ROOT = originalWorkspace;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { WriteFileHandler } from '../src/handlers/writeFile.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
|
+
* WriteFileHandler 测试类
|
|
11
|
+
* 测试文件写入功能
|
|
12
|
+
*/
|
|
13
|
+
describe('WriteFileHandler', () => {
|
|
14
|
+
|
|
15
|
+
const testDir = './test_files_write';
|
|
16
|
+
const testFile1 = join(testDir, 'test1.txt');
|
|
17
|
+
const testFile2 = join(testDir, 'test2.txt');
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
// 设置测试工作区
|
|
21
|
+
FileUtils.setWorkspaceRoot(process.cwd());
|
|
22
|
+
|
|
23
|
+
// 创建测试目录
|
|
24
|
+
if (!existsSync(testDir)) {
|
|
25
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
// 清理测试文件
|
|
31
|
+
try {
|
|
32
|
+
if (existsSync(testFile1)) await fs.unlink(testFile1);
|
|
33
|
+
if (existsSync(testFile2)) await fs.unlink(testFile2);
|
|
34
|
+
if (existsSync(testDir)) await fs.rmdir(testDir);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.warn('清理测试文件失败:', error.message);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('单文件写入', () => {
|
|
41
|
+
|
|
42
|
+
it('应该成功创建新文件', async () => {
|
|
43
|
+
const content = 'Line 1\nLine 2\nLine 3';
|
|
44
|
+
const args = {
|
|
45
|
+
path: testFile1,
|
|
46
|
+
content: content,
|
|
47
|
+
line_count: 3
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const result = await WriteFileHandler.handle(args);
|
|
51
|
+
|
|
52
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
53
|
+
assert.ok(result.content[0].text.includes('File written successfully'));
|
|
54
|
+
assert.ok(result.content[0].text.includes('3 lines'));
|
|
55
|
+
|
|
56
|
+
// 验证文件内容
|
|
57
|
+
const writtenContent = await fs.readFile(testFile1, 'utf8');
|
|
58
|
+
assert.strictEqual(writtenContent, content);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('应该覆盖现有文件', async () => {
|
|
62
|
+
// 先创建一个文件
|
|
63
|
+
await fs.writeFile(testFile1, 'Old content', 'utf8');
|
|
64
|
+
|
|
65
|
+
const newContent = 'New content\nSecond line';
|
|
66
|
+
const args = {
|
|
67
|
+
path: testFile1,
|
|
68
|
+
content: newContent,
|
|
69
|
+
line_count: 2
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const result = await WriteFileHandler.handle(args);
|
|
73
|
+
|
|
74
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
75
|
+
assert.ok(result.content[0].text.includes('File written successfully'));
|
|
76
|
+
|
|
77
|
+
// 验证文件内容被覆盖
|
|
78
|
+
const writtenContent = await fs.readFile(testFile1, 'utf8');
|
|
79
|
+
assert.strictEqual(writtenContent, newContent);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('应该处理空文件写入', async () => {
|
|
83
|
+
const args = {
|
|
84
|
+
path: testFile1,
|
|
85
|
+
content: '',
|
|
86
|
+
line_count: 0
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const result = await WriteFileHandler.handle(args);
|
|
90
|
+
|
|
91
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
92
|
+
assert.ok(result.content[0].text.includes('File written successfully'));
|
|
93
|
+
|
|
94
|
+
// 验证文件为空
|
|
95
|
+
const writtenContent = await fs.readFile(testFile1, 'utf8');
|
|
96
|
+
assert.strictEqual(writtenContent, '');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('应该发出警告当行数不匹配时', async () => {
|
|
100
|
+
const content = 'Line 1\nLine 2\nLine 3';
|
|
101
|
+
const args = {
|
|
102
|
+
path: testFile1,
|
|
103
|
+
content: content,
|
|
104
|
+
line_count: 5 // 错误的行数
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const result = await WriteFileHandler.handle(args);
|
|
108
|
+
|
|
109
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
110
|
+
assert.ok(result.content[0].text.includes('File written successfully'));
|
|
111
|
+
// 注意:警告是通过console.warn输出的,这里检查结果仍然成功
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('多文件写入', () => {
|
|
117
|
+
|
|
118
|
+
it('应该成功写入多个文件(相同内容)', async () => {
|
|
119
|
+
const content = 'Shared content\nLine 2';
|
|
120
|
+
const args = {
|
|
121
|
+
path: [testFile1, testFile2],
|
|
122
|
+
content: content,
|
|
123
|
+
line_count: 2
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const result = await WriteFileHandler.handle(args);
|
|
127
|
+
|
|
128
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
129
|
+
assert.ok(result.content[0].text.includes('Successfully wrote 2 files'));
|
|
130
|
+
assert.ok(result.content[0].text.includes('4 total lines'));
|
|
131
|
+
|
|
132
|
+
// 验证两个文件内容
|
|
133
|
+
const content1 = await fs.readFile(testFile1, 'utf8');
|
|
134
|
+
const content2 = await fs.readFile(testFile2, 'utf8');
|
|
135
|
+
assert.strictEqual(content1, content);
|
|
136
|
+
assert.strictEqual(content2, content);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('应该成功写入多个文件(不同内容)', async () => {
|
|
140
|
+
const content1 = 'File 1 content';
|
|
141
|
+
const content2 = 'File 2 content\nSecond line';
|
|
142
|
+
const args = {
|
|
143
|
+
path: [testFile1, testFile2],
|
|
144
|
+
content: [content1, content2],
|
|
145
|
+
line_count: [1, 2]
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const result = await WriteFileHandler.handle(args);
|
|
149
|
+
|
|
150
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
151
|
+
assert.ok(result.content[0].text.includes('Successfully wrote 2 files'));
|
|
152
|
+
assert.ok(result.content[0].text.includes('3 total lines'));
|
|
153
|
+
|
|
154
|
+
// 验证文件内容
|
|
155
|
+
const writtenContent1 = await fs.readFile(testFile1, 'utf8');
|
|
156
|
+
const writtenContent2 = await fs.readFile(testFile2, 'utf8');
|
|
157
|
+
assert.strictEqual(writtenContent1, content1);
|
|
158
|
+
assert.strictEqual(writtenContent2, content2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('应该抛出错误当路径和内容数组长度不匹配时', async () => {
|
|
162
|
+
const args = {
|
|
163
|
+
path: [testFile1, testFile2],
|
|
164
|
+
content: ['Content 1'], // 只有一个内容,但有两个路径
|
|
165
|
+
line_count: [1, 1]
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await assert.rejects(
|
|
169
|
+
() => WriteFileHandler.handle(args),
|
|
170
|
+
(error) => error.message.includes("doesn't match content count")
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('应该抛出错误当路径和行数数组长度不匹配时', async () => {
|
|
175
|
+
const args = {
|
|
176
|
+
path: [testFile1, testFile2],
|
|
177
|
+
content: ['Content 1', 'Content 2'],
|
|
178
|
+
line_count: [1] // 只有一个行数,但有两个路径
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
await assert.rejects(
|
|
182
|
+
() => WriteFileHandler.handle(args),
|
|
183
|
+
(error) => error.message.includes("doesn't match line_count array length")
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('错误处理', () => {
|
|
190
|
+
|
|
191
|
+
it('应该处理写入失败的情况', async () => {
|
|
192
|
+
// 使用无效路径
|
|
193
|
+
const invalidPath = '/invalid/path/that/does/not/exist/file.txt';
|
|
194
|
+
const args = {
|
|
195
|
+
path: invalidPath,
|
|
196
|
+
content: 'Some content',
|
|
197
|
+
line_count: 1
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await assert.rejects(
|
|
201
|
+
() => WriteFileHandler.handle(args),
|
|
202
|
+
(error) => error.message.includes('Failed to write file(s)')
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('应该处理部分文件写入失败的情况', async () => {
|
|
207
|
+
const validPath = testFile1;
|
|
208
|
+
const invalidPath = '/invalid/path/file.txt';
|
|
209
|
+
const args = {
|
|
210
|
+
path: [validPath, invalidPath],
|
|
211
|
+
content: ['Valid content', 'Invalid content'],
|
|
212
|
+
line_count: [1, 1]
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
await assert.rejects(
|
|
216
|
+
() => WriteFileHandler.handle(args),
|
|
217
|
+
(error) => error.message.includes('Failed to write') &&
|
|
218
|
+
error.message.includes('file(s)')
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('边界情况', () => {
|
|
225
|
+
|
|
226
|
+
it('应该处理包含特殊字符的内容', async () => {
|
|
227
|
+
const content = 'Special chars: áéíóú 中文 @#$%^&*()';
|
|
228
|
+
const args = {
|
|
229
|
+
path: testFile1,
|
|
230
|
+
content: content,
|
|
231
|
+
line_count: 1
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const result = await WriteFileHandler.handle(args);
|
|
235
|
+
|
|
236
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
237
|
+
assert.ok(result.content[0].text.includes('File written successfully'));
|
|
238
|
+
|
|
239
|
+
// 验证特殊字符被正确写入
|
|
240
|
+
const writtenContent = await fs.readFile(testFile1, 'utf8');
|
|
241
|
+
assert.strictEqual(writtenContent, content);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('应该处理大文件写入', async () => {
|
|
245
|
+
const bigContent = Array.from({ length: 1000 }, (_, i) => `Line ${i + 1}`).join('\n');
|
|
246
|
+
const args = {
|
|
247
|
+
path: testFile1,
|
|
248
|
+
content: bigContent,
|
|
249
|
+
line_count: 1000
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result = await WriteFileHandler.handle(args);
|
|
253
|
+
|
|
254
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
255
|
+
assert.ok(result.content[0].text.includes('File written successfully'));
|
|
256
|
+
assert.ok(result.content[0].text.includes('1000 lines'));
|
|
257
|
+
|
|
258
|
+
// 验证大文件内容
|
|
259
|
+
const writtenContent = await fs.readFile(testFile1, 'utf8');
|
|
260
|
+
assert.strictEqual(writtenContent, bigContent);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('应该创建不存在的目录', async () => {
|
|
264
|
+
const deepPath = join(testDir, 'deep', 'nested', 'file.txt');
|
|
265
|
+
const args = {
|
|
266
|
+
path: deepPath,
|
|
267
|
+
content: 'Deep file content',
|
|
268
|
+
line_count: 1
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const result = await WriteFileHandler.handle(args);
|
|
272
|
+
|
|
273
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
274
|
+
assert.ok(result.content[0].text.includes('File written successfully'));
|
|
275
|
+
|
|
276
|
+
// 验证文件和目录被创建
|
|
277
|
+
assert.ok(existsSync(deepPath));
|
|
278
|
+
const content = await fs.readFile(deepPath, 'utf8');
|
|
279
|
+
assert.strictEqual(content, 'Deep file content');
|
|
280
|
+
|
|
281
|
+
// 清理深层目录
|
|
282
|
+
await fs.unlink(deepPath);
|
|
283
|
+
await fs.rmdir(join(testDir, 'deep', 'nested'));
|
|
284
|
+
await fs.rmdir(join(testDir, 'deep'));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
});
|