@xiaozhi-client/cli 1.9.4-beta.10

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.
Files changed (55) hide show
  1. package/README.md +98 -0
  2. package/package.json +31 -0
  3. package/project.json +75 -0
  4. package/src/Constants.ts +105 -0
  5. package/src/Container.ts +212 -0
  6. package/src/Types.ts +79 -0
  7. package/src/commands/CommandHandlerFactory.ts +98 -0
  8. package/src/commands/ConfigCommandHandler.ts +279 -0
  9. package/src/commands/EndpointCommandHandler.ts +158 -0
  10. package/src/commands/McpCommandHandler.ts +778 -0
  11. package/src/commands/ProjectCommandHandler.ts +254 -0
  12. package/src/commands/ServiceCommandHandler.ts +182 -0
  13. package/src/commands/__tests__/CommandHandlerFactory.test.ts +323 -0
  14. package/src/commands/__tests__/CommandRegistry.test.ts +287 -0
  15. package/src/commands/__tests__/ConfigCommandHandler.test.ts +844 -0
  16. package/src/commands/__tests__/EndpointCommandHandler.test.ts +426 -0
  17. package/src/commands/__tests__/McpCommandHandler.test.ts +753 -0
  18. package/src/commands/__tests__/ProjectCommandHandler.test.ts +230 -0
  19. package/src/commands/__tests__/ServiceCommands.integration.test.ts +408 -0
  20. package/src/commands/index.ts +351 -0
  21. package/src/errors/ErrorHandlers.ts +141 -0
  22. package/src/errors/ErrorMessages.ts +121 -0
  23. package/src/errors/__tests__/index.test.ts +186 -0
  24. package/src/errors/index.ts +163 -0
  25. package/src/global.d.ts +19 -0
  26. package/src/index.ts +53 -0
  27. package/src/interfaces/Command.ts +128 -0
  28. package/src/interfaces/CommandTypes.ts +95 -0
  29. package/src/interfaces/Config.ts +25 -0
  30. package/src/interfaces/Service.ts +99 -0
  31. package/src/services/DaemonManager.ts +318 -0
  32. package/src/services/ProcessManager.ts +235 -0
  33. package/src/services/ServiceManager.ts +319 -0
  34. package/src/services/TemplateManager.ts +382 -0
  35. package/src/services/__tests__/DaemonManager.test.ts +378 -0
  36. package/src/services/__tests__/DaemonMode.integration.test.ts +321 -0
  37. package/src/services/__tests__/ProcessManager.test.ts +296 -0
  38. package/src/services/__tests__/ServiceManager.test.ts +774 -0
  39. package/src/services/__tests__/TemplateManager.test.ts +337 -0
  40. package/src/types/backend.d.ts +48 -0
  41. package/src/utils/FileUtils.ts +320 -0
  42. package/src/utils/FormatUtils.ts +198 -0
  43. package/src/utils/PathUtils.ts +255 -0
  44. package/src/utils/PlatformUtils.ts +217 -0
  45. package/src/utils/Validation.ts +274 -0
  46. package/src/utils/VersionUtils.ts +141 -0
  47. package/src/utils/__tests__/FileUtils.test.ts +728 -0
  48. package/src/utils/__tests__/FormatUtils.test.ts +243 -0
  49. package/src/utils/__tests__/PathUtils.test.ts +1165 -0
  50. package/src/utils/__tests__/PlatformUtils.test.ts +723 -0
  51. package/src/utils/__tests__/Validation.test.ts +560 -0
  52. package/src/utils/__tests__/VersionUtils.test.ts +410 -0
  53. package/tsconfig.json +32 -0
  54. package/tsup.config.ts +100 -0
  55. package/vitest.config.ts +97 -0
@@ -0,0 +1,728 @@
1
+ /**
2
+ * 文件操作工具单元测试
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
+ import { FileError } from "../../errors/index";
9
+ import { FileUtils } from "../FileUtils";
10
+
11
+ // Mock fs module
12
+ vi.mock("node:fs");
13
+ const mockedFs = vi.mocked(fs);
14
+
15
+ describe("FileUtils", () => {
16
+ const testDir = "/tmp/test";
17
+ const testFile = path.join(testDir, "test.txt");
18
+ const testContent = "Hello, World!";
19
+
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ });
27
+
28
+ describe("文件存在性检查", () => {
29
+ it("当文件存在时应返回 true", () => {
30
+ mockedFs.existsSync.mockReturnValue(true);
31
+ expect(FileUtils.exists(testFile)).toBe(true);
32
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testFile);
33
+ });
34
+
35
+ it("当文件不存在时应返回 false", () => {
36
+ mockedFs.existsSync.mockReturnValue(false);
37
+ expect(FileUtils.exists(testFile)).toBe(false);
38
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testFile);
39
+ });
40
+
41
+ it("发生异常时应返回 false", () => {
42
+ mockedFs.existsSync.mockImplementation(() => {
43
+ throw new Error("Permission denied");
44
+ });
45
+ expect(FileUtils.exists(testFile)).toBe(false);
46
+ });
47
+ });
48
+
49
+ describe("确保目录存在", () => {
50
+ it("当目录不存在时应创建目录", () => {
51
+ mockedFs.existsSync.mockReturnValue(false);
52
+ mockedFs.mkdirSync.mockImplementation(() => undefined);
53
+
54
+ FileUtils.ensureDir(testDir);
55
+
56
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testDir);
57
+ expect(mockedFs.mkdirSync).toHaveBeenCalledWith(testDir, {
58
+ recursive: true,
59
+ });
60
+ });
61
+
62
+ it("当目录已存在时不应创建目录", () => {
63
+ mockedFs.existsSync.mockReturnValue(true);
64
+
65
+ FileUtils.ensureDir(testDir);
66
+
67
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testDir);
68
+ expect(mockedFs.mkdirSync).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it("目录创建失败时应抛出文件错误", () => {
72
+ mockedFs.existsSync.mockReturnValue(false);
73
+ mockedFs.mkdirSync.mockImplementation(() => {
74
+ throw new Error("Permission denied");
75
+ });
76
+
77
+ expect(() => FileUtils.ensureDir(testDir)).toThrow(FileError);
78
+ expect(() => FileUtils.ensureDir(testDir)).toThrow("无法创建目录");
79
+ });
80
+ });
81
+
82
+ describe("读取文件", () => {
83
+ it("应成功读取文件内容", () => {
84
+ mockedFs.existsSync.mockReturnValue(true);
85
+ mockedFs.readFileSync.mockReturnValue(testContent);
86
+
87
+ const result = FileUtils.readFile(testFile);
88
+
89
+ expect(result).toBe(testContent);
90
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testFile);
91
+ expect(mockedFs.readFileSync).toHaveBeenCalledWith(testFile, "utf8");
92
+ });
93
+
94
+ it("文件不存在时应抛出文件错误", () => {
95
+ mockedFs.existsSync.mockReturnValue(false);
96
+
97
+ expect(() => FileUtils.readFile(testFile)).toThrow(FileError);
98
+ expect(() => FileUtils.readFile(testFile)).toThrow("文件不存在");
99
+ });
100
+
101
+ it("文件读取失败时应抛出文件错误", () => {
102
+ mockedFs.existsSync.mockReturnValue(true);
103
+ mockedFs.readFileSync.mockImplementation(() => {
104
+ throw new Error("Read error");
105
+ });
106
+
107
+ expect(() => FileUtils.readFile(testFile)).toThrow(FileError);
108
+ expect(() => FileUtils.readFile(testFile)).toThrow("无法读取文件");
109
+ });
110
+
111
+ it("应使用自定义编码", () => {
112
+ mockedFs.existsSync.mockReturnValue(true);
113
+ mockedFs.readFileSync.mockReturnValue(testContent);
114
+
115
+ FileUtils.readFile(testFile, "ascii");
116
+
117
+ expect(mockedFs.readFileSync).toHaveBeenCalledWith(testFile, "ascii");
118
+ });
119
+ });
120
+
121
+ describe("写入文件", () => {
122
+ beforeEach(() => {
123
+ // Mock path.dirname to return correct directory for test file
124
+ vi.spyOn(path, "dirname").mockImplementation((filePath) => {
125
+ if (filePath === testFile) {
126
+ return testDir;
127
+ }
128
+ return path.dirname(filePath);
129
+ });
130
+ });
131
+
132
+ it("当覆盖为 true 时应成功写入文件", () => {
133
+ mockedFs.existsSync.mockReturnValue(true);
134
+ mockedFs.writeFileSync.mockImplementation(() => {});
135
+
136
+ FileUtils.writeFile(testFile, testContent, { overwrite: true });
137
+
138
+ // The key thing is that the file gets written successfully
139
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith(
140
+ testFile,
141
+ testContent,
142
+ "utf8"
143
+ );
144
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith(
145
+ testFile,
146
+ testContent,
147
+ "utf8"
148
+ );
149
+ });
150
+
151
+ it("文件不存在时应成功写入文件", () => {
152
+ mockedFs.existsSync.mockReturnValue(false);
153
+ mockedFs.mkdirSync.mockImplementation(() => undefined);
154
+ mockedFs.writeFileSync.mockImplementation(() => {});
155
+
156
+ FileUtils.writeFile(testFile, testContent);
157
+
158
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testFile);
159
+ expect(mockedFs.mkdirSync).toHaveBeenCalled();
160
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith(
161
+ testFile,
162
+ testContent,
163
+ "utf8"
164
+ );
165
+ });
166
+
167
+ it("当文件存在且覆盖为 false 时应抛出文件错误", () => {
168
+ mockedFs.existsSync.mockReturnValue(true);
169
+
170
+ expect(() => FileUtils.writeFile(testFile, testContent)).toThrow(
171
+ FileError
172
+ );
173
+ expect(() => FileUtils.writeFile(testFile, testContent)).toThrow(
174
+ "文件已存在"
175
+ );
176
+ });
177
+
178
+ it("写入失败时应抛出文件错误", () => {
179
+ mockedFs.existsSync.mockReturnValue(false);
180
+ mockedFs.mkdirSync.mockImplementation(() => undefined);
181
+ mockedFs.writeFileSync.mockImplementation(() => {
182
+ throw new Error("Write error");
183
+ });
184
+
185
+ expect(() => FileUtils.writeFile(testFile, testContent)).toThrow(
186
+ FileError
187
+ );
188
+ expect(() => FileUtils.writeFile(testFile, testContent)).toThrow(
189
+ "无法写入文件"
190
+ );
191
+ });
192
+
193
+ it("应使用默认覆盖选项 false", () => {
194
+ mockedFs.existsSync.mockReturnValue(false);
195
+ mockedFs.mkdirSync.mockImplementation(() => undefined);
196
+ mockedFs.writeFileSync.mockImplementation(() => {});
197
+
198
+ FileUtils.writeFile(testFile, testContent);
199
+
200
+ expect(mockedFs.writeFileSync).toHaveBeenCalled();
201
+ });
202
+ });
203
+
204
+ describe("复制文件", () => {
205
+ const destFile = path.join(testDir, "copy.txt");
206
+
207
+ beforeEach(() => {
208
+ // Mock path.dirname
209
+ vi.spyOn(path, "dirname").mockReturnValue(testDir);
210
+ });
211
+
212
+ it("当覆盖为 true 时应成功复制文件", () => {
213
+ // Configure mocks for copyFile execution
214
+ mockedFs.existsSync.mockReturnValue(true); // All files and directories exist
215
+ mockedFs.copyFileSync.mockImplementation(() => {});
216
+
217
+ FileUtils.copyFile(testFile, destFile, { overwrite: true });
218
+
219
+ // Check that the key operations happened
220
+ expect(mockedFs.copyFileSync).toHaveBeenCalledWith(testFile, destFile);
221
+ });
222
+
223
+ it("目标文件不存在时应成功复制文件", () => {
224
+ mockedFs.existsSync.mockReturnValueOnce(true).mockReturnValueOnce(false);
225
+ mockedFs.mkdirSync.mockImplementation(() => undefined);
226
+ mockedFs.copyFileSync.mockImplementation(() => {});
227
+
228
+ FileUtils.copyFile(testFile, destFile);
229
+
230
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testFile);
231
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(destFile);
232
+ expect(mockedFs.mkdirSync).toHaveBeenCalled();
233
+ expect(mockedFs.copyFileSync).toHaveBeenCalledWith(testFile, destFile);
234
+ });
235
+
236
+ it("源文件不存在时应抛出文件错误", () => {
237
+ mockedFs.existsSync.mockReturnValue(false);
238
+
239
+ expect(() => FileUtils.copyFile(testFile, destFile)).toThrow(FileError);
240
+ expect(() => FileUtils.copyFile(testFile, destFile)).toThrow(
241
+ "文件不存在"
242
+ );
243
+ });
244
+
245
+ it("目标文件存在且覆盖为 false 时应抛出文件错误", () => {
246
+ mockedFs.existsSync.mockReturnValue(true);
247
+
248
+ expect(() => FileUtils.copyFile(testFile, destFile)).toThrow(FileError);
249
+ expect(() => FileUtils.copyFile(testFile, destFile)).toThrow(
250
+ "文件已存在"
251
+ );
252
+ });
253
+
254
+ it("复制失败时应抛出文件错误", () => {
255
+ mockedFs.existsSync.mockImplementation((filePath) => {
256
+ if (filePath === testFile) return true; // Source file exists
257
+ if (filePath === destFile) return false; // Destination doesn't exist
258
+ return false; // Other files don't exist
259
+ });
260
+ mockedFs.mkdirSync.mockImplementation(() => undefined);
261
+ mockedFs.copyFileSync.mockImplementation(() => {
262
+ throw new Error("Copy error");
263
+ });
264
+
265
+ expect(() => FileUtils.copyFile(testFile, destFile)).toThrow(FileError);
266
+ expect(() => FileUtils.copyFile(testFile, destFile)).toThrow(
267
+ "无法复制文件"
268
+ );
269
+ });
270
+ });
271
+
272
+ describe("删除文件", () => {
273
+ it("文件存在时应删除文件", () => {
274
+ mockedFs.existsSync.mockReturnValue(true);
275
+ mockedFs.unlinkSync.mockImplementation(() => {});
276
+
277
+ FileUtils.deleteFile(testFile);
278
+
279
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testFile);
280
+ expect(mockedFs.unlinkSync).toHaveBeenCalledWith(testFile);
281
+ });
282
+
283
+ it("文件不存在时不应尝试删除", () => {
284
+ mockedFs.existsSync.mockReturnValue(false);
285
+
286
+ FileUtils.deleteFile(testFile);
287
+
288
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testFile);
289
+ expect(mockedFs.unlinkSync).not.toHaveBeenCalled();
290
+ });
291
+
292
+ it("文件删除失败时应抛出文件错误", () => {
293
+ mockedFs.existsSync.mockReturnValue(true);
294
+ mockedFs.unlinkSync.mockImplementation(() => {
295
+ throw new Error("Delete error");
296
+ });
297
+
298
+ expect(() => FileUtils.deleteFile(testFile)).toThrow(FileError);
299
+ expect(() => FileUtils.deleteFile(testFile)).toThrow("无法删除文件");
300
+ });
301
+ });
302
+
303
+ describe("复制目录", () => {
304
+ const srcDir = "/tmp/source";
305
+ const destDir = "/tmp/destination";
306
+ const mockStats = {
307
+ isDirectory: () => false,
308
+ isFile: () => true,
309
+ isBlockDevice: () => false,
310
+ isCharacterDevice: () => false,
311
+ isSymbolicLink: () => false,
312
+ isFIFO: () => false,
313
+ isSocket: () => false,
314
+ dev: 0n,
315
+ ino: 0n,
316
+ mode: 0n,
317
+ nlink: 0n,
318
+ uid: 0n,
319
+ gid: 0n,
320
+ rdev: 0n,
321
+ size: 0n,
322
+ blksize: 0n,
323
+ blocks: 0n,
324
+ atimeMs: 0n,
325
+ mtimeMs: 0n,
326
+ ctimeMs: 0n,
327
+ birthtimeMs: 0n,
328
+ atimeNs: 0n,
329
+ mtimeNs: 0n,
330
+ ctimeNs: 0n,
331
+ birthtimeNs: 0n,
332
+ atime: new Date(),
333
+ mtime: new Date(),
334
+ ctime: new Date(),
335
+ birthtime: new Date(),
336
+ };
337
+
338
+ beforeEach(() => {
339
+ // Mock path.dirname to return correct directory for each file
340
+ vi.spyOn(path, "dirname").mockImplementation((filePath) => {
341
+ if (filePath.includes(destDir)) {
342
+ return destDir;
343
+ }
344
+ return path.dirname(filePath);
345
+ });
346
+ });
347
+
348
+ it("源目录不存在时应抛出文件错误", () => {
349
+ mockedFs.existsSync.mockReturnValue(false);
350
+
351
+ expect(() => FileUtils.copyDirectory(srcDir, destDir)).toThrow(FileError);
352
+ expect(() => FileUtils.copyDirectory(srcDir, destDir)).toThrow(
353
+ "文件不存在"
354
+ );
355
+ });
356
+ });
357
+
358
+ describe("删除目录", () => {
359
+ it("目录存在时应删除目录", () => {
360
+ mockedFs.existsSync.mockReturnValue(true);
361
+ mockedFs.rmSync.mockImplementation(() => {});
362
+
363
+ FileUtils.deleteDirectory(testDir);
364
+
365
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testDir);
366
+ expect(mockedFs.rmSync).toHaveBeenCalledWith(testDir, {
367
+ recursive: true,
368
+ force: true,
369
+ });
370
+ });
371
+
372
+ it("目录不存在时不应尝试删除", () => {
373
+ mockedFs.existsSync.mockReturnValue(false);
374
+
375
+ FileUtils.deleteDirectory(testDir);
376
+
377
+ expect(mockedFs.existsSync).toHaveBeenCalledWith(testDir);
378
+ expect(mockedFs.rmSync).not.toHaveBeenCalled();
379
+ });
380
+
381
+ it("应使用自定义递归选项", () => {
382
+ mockedFs.existsSync.mockReturnValue(true);
383
+ mockedFs.rmSync.mockImplementation(() => {});
384
+
385
+ FileUtils.deleteDirectory(testDir, { recursive: false });
386
+
387
+ expect(mockedFs.rmSync).toHaveBeenCalledWith(testDir, {
388
+ recursive: false,
389
+ force: true,
390
+ });
391
+ });
392
+
393
+ it("目录删除失败时应抛出文件错误", () => {
394
+ mockedFs.existsSync.mockReturnValue(true);
395
+ mockedFs.rmSync.mockImplementation(() => {
396
+ throw new Error("Delete error");
397
+ });
398
+
399
+ expect(() => FileUtils.deleteDirectory(testDir)).toThrow(FileError);
400
+ expect(() => FileUtils.deleteDirectory(testDir)).toThrow("无法删除目录");
401
+ });
402
+ });
403
+
404
+ describe("获取文件信息", () => {
405
+ const mockStats = {
406
+ size: 1024n,
407
+ isFile: () => true,
408
+ isDirectory: () => false,
409
+ isBlockDevice: () => false,
410
+ isCharacterDevice: () => false,
411
+ isSymbolicLink: () => false,
412
+ isFIFO: () => false,
413
+ isSocket: () => false,
414
+ mtime: new Date("2023-01-01"),
415
+ ctime: new Date("2023-01-01"),
416
+ dev: 0n,
417
+ ino: 0n,
418
+ mode: 0n,
419
+ nlink: 0n,
420
+ uid: 0n,
421
+ gid: 0n,
422
+ rdev: 0n,
423
+ blksize: 0n,
424
+ blocks: 0n,
425
+ atimeMs: 0n,
426
+ mtimeMs: 0n,
427
+ ctimeMs: 0n,
428
+ birthtimeMs: 0n,
429
+ atimeNs: 0n,
430
+ mtimeNs: 0n,
431
+ ctimeNs: 0n,
432
+ birthtimeNs: 0n,
433
+ atime: new Date(),
434
+ birthtime: new Date(),
435
+ };
436
+
437
+ it("应成功获取文件信息", () => {
438
+ mockedFs.existsSync.mockReturnValue(true);
439
+ mockedFs.statSync.mockReturnValue(mockStats);
440
+
441
+ const result = FileUtils.getFileInfo(testFile);
442
+
443
+ expect(result).toEqual({
444
+ size: 1024n,
445
+ isFile: true,
446
+ isDirectory: false,
447
+ mtime: mockStats.mtime,
448
+ ctime: mockStats.ctime,
449
+ });
450
+ });
451
+
452
+ it("文件不存在时应抛出文件错误", () => {
453
+ mockedFs.existsSync.mockReturnValue(false);
454
+
455
+ expect(() => FileUtils.getFileInfo(testFile)).toThrow(FileError);
456
+ expect(() => FileUtils.getFileInfo(testFile)).toThrow("文件不存在");
457
+ });
458
+
459
+ it("获取状态失败时应抛出文件错误", () => {
460
+ mockedFs.existsSync.mockReturnValue(true);
461
+ mockedFs.statSync.mockImplementation(() => {
462
+ throw new Error("Stat error");
463
+ });
464
+
465
+ expect(() => FileUtils.getFileInfo(testFile)).toThrow(FileError);
466
+ expect(() => FileUtils.getFileInfo(testFile)).toThrow("无法获取文件信息");
467
+ });
468
+ });
469
+
470
+ describe("列出目录内容", () => {
471
+ const mockStats = {
472
+ isDirectory: () => false,
473
+ isFile: () => true,
474
+ isBlockDevice: () => false,
475
+ isCharacterDevice: () => false,
476
+ isSymbolicLink: () => false,
477
+ isFIFO: () => false,
478
+ isSocket: () => false,
479
+ dev: 0n,
480
+ ino: 0n,
481
+ mode: 0n,
482
+ nlink: 0n,
483
+ uid: 0n,
484
+ gid: 0n,
485
+ rdev: 0n,
486
+ size: 0n,
487
+ blksize: 0n,
488
+ blocks: 0n,
489
+ atimeMs: 0n,
490
+ mtimeMs: 0n,
491
+ ctimeMs: 0n,
492
+ birthtimeMs: 0n,
493
+ atimeNs: 0n,
494
+ mtimeNs: 0n,
495
+ ctimeNs: 0n,
496
+ birthtimeNs: 0n,
497
+ atime: new Date(),
498
+ mtime: new Date(),
499
+ ctime: new Date(),
500
+ birthtime: new Date(),
501
+ };
502
+
503
+ it("应成功列出目录内容", () => {
504
+ mockedFs.existsSync.mockReturnValue(true);
505
+ mockedFs.readdirSync.mockReturnValue(["file1.txt", "file2.txt"] as any);
506
+ mockedFs.statSync.mockReturnValue(mockStats);
507
+
508
+ const result = FileUtils.listDirectory(testDir);
509
+
510
+ expect(result).toHaveLength(2);
511
+ expect(result[0]).toBe(path.join(testDir, "file1.txt"));
512
+ expect(result[1]).toBe(path.join(testDir, "file2.txt"));
513
+ });
514
+
515
+ it("目录不存在时应抛出文件错误", () => {
516
+ mockedFs.existsSync.mockReturnValue(false);
517
+
518
+ expect(() => FileUtils.listDirectory(testDir)).toThrow(FileError);
519
+ expect(() => FileUtils.listDirectory(testDir)).toThrow("文件不存在");
520
+ });
521
+
522
+ it("默认情况下应跳过隐藏文件", () => {
523
+ mockedFs.existsSync.mockReturnValue(true);
524
+ mockedFs.readdirSync.mockReturnValue([
525
+ "file1.txt",
526
+ ".hidden",
527
+ "file2.txt",
528
+ ] as any);
529
+ mockedFs.statSync.mockReturnValue(mockStats);
530
+
531
+ const result = FileUtils.listDirectory(testDir);
532
+
533
+ expect(result).toHaveLength(2);
534
+ expect(result.some((item) => item.includes(".hidden"))).toBe(false);
535
+ });
536
+
537
+ it("指定时应包含隐藏文件", () => {
538
+ mockedFs.existsSync.mockReturnValue(true);
539
+ mockedFs.readdirSync.mockReturnValue([
540
+ "file1.txt",
541
+ ".hidden",
542
+ "file2.txt",
543
+ ] as any);
544
+ mockedFs.statSync.mockReturnValue(mockStats);
545
+
546
+ const result = FileUtils.listDirectory(testDir, { includeHidden: true });
547
+
548
+ expect(result).toHaveLength(3);
549
+ expect(result.some((item) => item.includes(".hidden"))).toBe(true);
550
+ });
551
+
552
+ it("应处理递归列出", () => {
553
+ const subDir = path.join(testDir, "subdir");
554
+ const subFile = path.join(subDir, "subfile.txt");
555
+
556
+ mockedFs.existsSync.mockReturnValue(true);
557
+ mockedFs.readdirSync
558
+ .mockReturnValueOnce(["file1.txt", "subdir"] as any)
559
+ .mockReturnValueOnce(["subfile.txt"] as any);
560
+ mockedFs.statSync
561
+ .mockReturnValueOnce({
562
+ isDirectory: () => false,
563
+ isFile: () => true,
564
+ isBlockDevice: () => false,
565
+ isCharacterDevice: () => false,
566
+ isSymbolicLink: () => false,
567
+ isFIFO: () => false,
568
+ isSocket: () => false,
569
+ dev: 0n,
570
+ ino: 0n,
571
+ mode: 0n,
572
+ nlink: 0n,
573
+ uid: 0n,
574
+ gid: 0n,
575
+ rdev: 0n,
576
+ size: 0n,
577
+ blksize: 0n,
578
+ blocks: 0n,
579
+ atimeMs: 0n,
580
+ mtimeMs: 0n,
581
+ ctimeMs: 0n,
582
+ birthtimeMs: 0n,
583
+ atimeNs: 0n,
584
+ mtimeNs: 0n,
585
+ ctimeNs: 0n,
586
+ birthtimeNs: 0n,
587
+ atime: new Date(),
588
+ mtime: new Date(),
589
+ ctime: new Date(),
590
+ birthtime: new Date(),
591
+ })
592
+ .mockReturnValueOnce({
593
+ isDirectory: () => true,
594
+ isFile: () => false,
595
+ isBlockDevice: () => false,
596
+ isCharacterDevice: () => false,
597
+ isSymbolicLink: () => false,
598
+ isFIFO: () => false,
599
+ isSocket: () => false,
600
+ dev: 0n,
601
+ ino: 0n,
602
+ mode: 0n,
603
+ nlink: 0n,
604
+ uid: 0n,
605
+ gid: 0n,
606
+ rdev: 0n,
607
+ size: 0n,
608
+ blksize: 0n,
609
+ blocks: 0n,
610
+ atimeMs: 0n,
611
+ mtimeMs: 0n,
612
+ ctimeMs: 0n,
613
+ birthtimeMs: 0n,
614
+ atimeNs: 0n,
615
+ mtimeNs: 0n,
616
+ ctimeNs: 0n,
617
+ birthtimeNs: 0n,
618
+ atime: new Date(),
619
+ mtime: new Date(),
620
+ ctime: new Date(),
621
+ birthtime: new Date(),
622
+ })
623
+ .mockReturnValueOnce({
624
+ isDirectory: () => false,
625
+ isFile: () => true,
626
+ isBlockDevice: () => false,
627
+ isCharacterDevice: () => false,
628
+ isSymbolicLink: () => false,
629
+ isFIFO: () => false,
630
+ isSocket: () => false,
631
+ dev: 0n,
632
+ ino: 0n,
633
+ mode: 0n,
634
+ nlink: 0n,
635
+ uid: 0n,
636
+ gid: 0n,
637
+ rdev: 0n,
638
+ size: 0n,
639
+ blksize: 0n,
640
+ blocks: 0n,
641
+ atimeMs: 0n,
642
+ mtimeMs: 0n,
643
+ ctimeMs: 0n,
644
+ birthtimeMs: 0n,
645
+ atimeNs: 0n,
646
+ mtimeNs: 0n,
647
+ ctimeNs: 0n,
648
+ birthtimeNs: 0n,
649
+ atime: new Date(),
650
+ mtime: new Date(),
651
+ ctime: new Date(),
652
+ birthtime: new Date(),
653
+ });
654
+
655
+ const result = FileUtils.listDirectory(testDir, { recursive: true });
656
+
657
+ expect(result).toHaveLength(3);
658
+ expect(result).toContain(subFile);
659
+ });
660
+ });
661
+
662
+ describe("检查权限", () => {
663
+ it("权限足够时应返回 true", () => {
664
+ mockedFs.accessSync.mockImplementation(() => {});
665
+
666
+ const result = FileUtils.checkPermissions(testFile);
667
+
668
+ expect(result).toBe(true);
669
+ expect(mockedFs.accessSync).toHaveBeenCalledWith(
670
+ testFile,
671
+ fs.constants.R_OK | fs.constants.W_OK
672
+ );
673
+ });
674
+
675
+ it("权限不足时应返回 false", () => {
676
+ mockedFs.accessSync.mockImplementation(() => {
677
+ throw new Error("Permission denied");
678
+ });
679
+
680
+ const result = FileUtils.checkPermissions(testFile);
681
+
682
+ expect(result).toBe(false);
683
+ });
684
+
685
+ it("应使用自定义权限模式", () => {
686
+ mockedFs.accessSync.mockImplementation(() => {});
687
+
688
+ FileUtils.checkPermissions(testFile, fs.constants.R_OK);
689
+
690
+ expect(mockedFs.accessSync).toHaveBeenCalledWith(
691
+ testFile,
692
+ fs.constants.R_OK
693
+ );
694
+ });
695
+ });
696
+
697
+ describe("获取文件扩展名", () => {
698
+ it("应返回小写的文件扩展名", () => {
699
+ expect(FileUtils.getExtension("test.TXT")).toBe(".txt");
700
+ expect(FileUtils.getExtension("archive.tar.gz")).toBe(".gz");
701
+ expect(FileUtils.getExtension("filename")).toBe("");
702
+ expect(FileUtils.getExtension("")).toBe("");
703
+ });
704
+ });
705
+
706
+ describe("获取文件基本名称", () => {
707
+ it("应返回不带扩展名的文件名", () => {
708
+ expect(FileUtils.getBaseName("test.txt")).toBe("test");
709
+ expect(FileUtils.getBaseName("archive.tar.gz")).toBe("archive.tar");
710
+ expect(FileUtils.getBaseName("filename")).toBe("filename");
711
+ expect(FileUtils.getBaseName("/path/to/test.txt")).toBe("test");
712
+ });
713
+ });
714
+
715
+ describe("解析路径", () => {
716
+ it("应将相对路径解析为绝对路径", () => {
717
+ const result = FileUtils.resolvePath("relative/path", "/base");
718
+
719
+ expect(result).toBe(path.resolve("/base", "relative/path"));
720
+ });
721
+
722
+ it("应解析不带基路径的路径", () => {
723
+ const result = FileUtils.resolvePath("relative/path");
724
+
725
+ expect(result).toBe(path.resolve("relative/path"));
726
+ });
727
+ });
728
+ });