@xiaozhi-client/cli 2.0.0 → 2.1.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/package.json +2 -2
- package/src/Container.ts +8 -0
- package/src/commands/CommandHandlerFactory.ts +9 -0
- package/src/commands/ConfigCommandHandler.ts +22 -14
- package/src/services/DaemonManager.ts +8 -0
- package/src/services/ServiceManager.ts +8 -0
- package/src/services/TemplateManager.ts +9 -0
- package/src/utils/__tests__/path-utils.config.test.ts +214 -0
- package/src/utils/__tests__/path-utils.executable.test.ts +424 -0
- package/src/utils/__tests__/path-utils.filesystem.test.ts +278 -0
- package/src/utils/__tests__/path-utils.security.test.ts +357 -0
- package/src/utils/__tests__/path-utils.test.ts +0 -1165
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { PathUtils } from "../PathUtils.js";
|
|
6
|
+
|
|
7
|
+
// Mock dependencies - 需要使用与源文件相同的导入方式
|
|
8
|
+
vi.mock("node:url", () => ({
|
|
9
|
+
fileURLToPath: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("node:fs", () => ({
|
|
13
|
+
realpathSync: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock process.argv and environment variables
|
|
17
|
+
const originalArgv = process.argv;
|
|
18
|
+
const originalEnv = process.env;
|
|
19
|
+
|
|
20
|
+
describe("PathUtils - 可执行文件路径", () => {
|
|
21
|
+
let mockRealpathSync: any;
|
|
22
|
+
let mockFileURLToPath: any;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
|
|
27
|
+
// Setup mocks
|
|
28
|
+
mockRealpathSync = vi.mocked(realpathSync);
|
|
29
|
+
mockFileURLToPath = vi.mocked(fileURLToPath);
|
|
30
|
+
|
|
31
|
+
// Default mock implementations
|
|
32
|
+
mockRealpathSync.mockImplementation((path: string) => path); // 默认返回原路径
|
|
33
|
+
mockFileURLToPath.mockReturnValue("/test/src/cli/utils/PathUtils.js");
|
|
34
|
+
|
|
35
|
+
// Reset environment variables
|
|
36
|
+
process.env = { ...originalEnv };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
process.argv = originalArgv;
|
|
41
|
+
process.env = originalEnv;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("getExecutablePath 获取可执行文件路径", () => {
|
|
45
|
+
describe("基本路径解析", () => {
|
|
46
|
+
it("应该基于当前 CLI 脚本位置返回正确路径", () => {
|
|
47
|
+
// Mock process.argv[1] to simulate CLI script path
|
|
48
|
+
process.argv = ["node", "/Users/test/xiaozhi-client/dist/cli.js"];
|
|
49
|
+
mockRealpathSync.mockReturnValue(
|
|
50
|
+
"/Users/test/xiaozhi-client/dist/cli.js"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const result = PathUtils.getExecutablePath("WebServerLauncher");
|
|
54
|
+
const expected = path.join(
|
|
55
|
+
"/Users/test/xiaozhi-client/dist",
|
|
56
|
+
"WebServerLauncher.js"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(result).toBe(expected);
|
|
60
|
+
expect(mockRealpathSync).toHaveBeenCalledWith(
|
|
61
|
+
"/Users/test/xiaozhi-client/dist/cli.js"
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("应该处理不同的 CLI 脚本位置", () => {
|
|
66
|
+
process.argv = ["node", "/opt/xiaozhi/dist/cli.js"];
|
|
67
|
+
mockRealpathSync.mockReturnValue("/opt/xiaozhi/dist/cli.js");
|
|
68
|
+
|
|
69
|
+
const result = PathUtils.getExecutablePath("WebServerLauncher");
|
|
70
|
+
const expected = path.join("/opt/xiaozhi/dist", "WebServerLauncher.js");
|
|
71
|
+
|
|
72
|
+
expect(result).toBe(expected);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("应该支持相对路径", () => {
|
|
76
|
+
process.argv = ["node", "./dist/cli.js"];
|
|
77
|
+
mockRealpathSync.mockReturnValue("./dist/cli.js");
|
|
78
|
+
|
|
79
|
+
const result = PathUtils.getExecutablePath("test");
|
|
80
|
+
const expected = path.join("./dist", "test.js");
|
|
81
|
+
|
|
82
|
+
expect(result).toBe(expected);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("符号链接解析测试", () => {
|
|
87
|
+
it("应该正确解析符号链接到真实路径", () => {
|
|
88
|
+
// 模拟 npm 全局安装的符号链接场景
|
|
89
|
+
const symlinkPath =
|
|
90
|
+
"/Users/nemo/.nvm/versions/node/v20.19.2/bin/xiaozhi";
|
|
91
|
+
const realPath =
|
|
92
|
+
"/Users/nemo/.nvm/versions/node/v20.19.2/lib/node_modules/xiaozhi-client/dist/cli.js";
|
|
93
|
+
|
|
94
|
+
process.argv = ["node", symlinkPath];
|
|
95
|
+
mockRealpathSync.mockReturnValue(realPath);
|
|
96
|
+
|
|
97
|
+
const result = PathUtils.getExecutablePath("WebServerLauncher");
|
|
98
|
+
const expected = path.join(
|
|
99
|
+
"/Users/nemo/.nvm/versions/node/v20.19.2/lib/node_modules/xiaozhi-client/dist",
|
|
100
|
+
"WebServerLauncher.js"
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result).toBe(expected);
|
|
104
|
+
expect(mockRealpathSync).toHaveBeenCalledWith(symlinkPath);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("应该处理多层符号链接", () => {
|
|
108
|
+
const symlinkPath = "/usr/local/bin/xiaozhi";
|
|
109
|
+
const realPath = "/opt/xiaozhi-client/dist/cli.js";
|
|
110
|
+
|
|
111
|
+
process.argv = ["node", symlinkPath];
|
|
112
|
+
mockRealpathSync.mockReturnValue(realPath);
|
|
113
|
+
|
|
114
|
+
const result = PathUtils.getExecutablePath("WebServerLauncher");
|
|
115
|
+
const expected = path.join(
|
|
116
|
+
"/opt/xiaozhi-client/dist",
|
|
117
|
+
"WebServerLauncher.js"
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(result).toBe(expected);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("应该在符号链接解析失败时回退到原路径", () => {
|
|
124
|
+
const symlinkPath = "/broken/symlink/xiaozhi";
|
|
125
|
+
|
|
126
|
+
process.argv = ["node", symlinkPath];
|
|
127
|
+
mockRealpathSync.mockImplementation(() => {
|
|
128
|
+
throw new Error("ENOENT: no such file or directory");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = PathUtils.getExecutablePath("WebServerLauncher");
|
|
132
|
+
const expected = path.join("/broken/symlink", "WebServerLauncher.js");
|
|
133
|
+
|
|
134
|
+
expect(result).toBe(expected);
|
|
135
|
+
expect(mockRealpathSync).toHaveBeenCalledWith(symlinkPath);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("应该处理权限错误导致的符号链接解析失败", () => {
|
|
139
|
+
const symlinkPath = "/restricted/xiaozhi";
|
|
140
|
+
|
|
141
|
+
process.argv = ["node", symlinkPath];
|
|
142
|
+
mockRealpathSync.mockImplementation(() => {
|
|
143
|
+
throw new Error("EACCES: permission denied");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = PathUtils.getExecutablePath("test");
|
|
147
|
+
const expected = path.join("/restricted", "test.js");
|
|
148
|
+
|
|
149
|
+
expect(result).toBe(expected);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("路径构建测试", () => {
|
|
154
|
+
it("应该正确构建不同可执行文件名称的路径", () => {
|
|
155
|
+
process.argv = ["node", "/test/dist/cli.js"];
|
|
156
|
+
mockRealpathSync.mockReturnValue("/test/dist/cli.js");
|
|
157
|
+
|
|
158
|
+
const testCases = [
|
|
159
|
+
{ name: "WebServerLauncher", expected: "WebServerLauncher.js" },
|
|
160
|
+
{ name: "customScript", expected: "customScript.js" },
|
|
161
|
+
{ name: "app", expected: "app.js" },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
for (const { name, expected } of testCases) {
|
|
165
|
+
const result = PathUtils.getExecutablePath(name);
|
|
166
|
+
expect(result).toBe(path.join("/test/dist", expected));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("应该确保返回的路径以 .js 结尾", () => {
|
|
171
|
+
process.argv = ["node", "/test/dist/cli.js"];
|
|
172
|
+
mockRealpathSync.mockReturnValue("/test/dist/cli.js");
|
|
173
|
+
|
|
174
|
+
const testNames = ["script", "app.exe", "tool.bin", "service"];
|
|
175
|
+
|
|
176
|
+
for (const name of testNames) {
|
|
177
|
+
const result = PathUtils.getExecutablePath(name);
|
|
178
|
+
expect(result).toMatch(/\.js$/);
|
|
179
|
+
expect(result).toContain(name);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("边界情况测试", () => {
|
|
185
|
+
it("应该处理空的可执行文件名", () => {
|
|
186
|
+
process.argv = ["node", "/test/dist/cli.js"];
|
|
187
|
+
mockRealpathSync.mockReturnValue("/test/dist/cli.js");
|
|
188
|
+
|
|
189
|
+
const result = PathUtils.getExecutablePath("");
|
|
190
|
+
const expected = path.join("/test/dist", ".js");
|
|
191
|
+
|
|
192
|
+
expect(result).toBe(expected);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("应该处理 process.argv[1] 为 undefined 的情况", () => {
|
|
196
|
+
process.argv = ["node"]; // 没有第二个参数
|
|
197
|
+
|
|
198
|
+
const result = PathUtils.getExecutablePath("test");
|
|
199
|
+
const expected = path.join(process.cwd(), "test.js");
|
|
200
|
+
|
|
201
|
+
expect(result).toBe(expected);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("应该处理非常长的路径", () => {
|
|
205
|
+
const longPath = `/very/long/path/${"a".repeat(200)}/dist/cli.js`;
|
|
206
|
+
process.argv = ["node", longPath];
|
|
207
|
+
mockRealpathSync.mockReturnValue(longPath);
|
|
208
|
+
|
|
209
|
+
const result = PathUtils.getExecutablePath("test");
|
|
210
|
+
|
|
211
|
+
expect(result).toContain("test.js");
|
|
212
|
+
expect(result.length).toBeGreaterThan(200);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("应该处理包含特殊字符的路径", () => {
|
|
216
|
+
const specialPath = "/path with spaces/special-chars_123/dist/cli.js";
|
|
217
|
+
process.argv = ["node", specialPath];
|
|
218
|
+
mockRealpathSync.mockReturnValue(specialPath);
|
|
219
|
+
|
|
220
|
+
const result = PathUtils.getExecutablePath("test");
|
|
221
|
+
const expected = path.join(
|
|
222
|
+
"/path with spaces/special-chars_123/dist",
|
|
223
|
+
"test.js"
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(result).toBe(expected);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("集成测试", () => {
|
|
231
|
+
it("应该模拟真实的 npm 全局安装环境", () => {
|
|
232
|
+
// 模拟真实的 npm 全局安装路径结构
|
|
233
|
+
const npmBinPath =
|
|
234
|
+
"/Users/user/.nvm/versions/node/v18.17.0/bin/xiaozhi";
|
|
235
|
+
const npmRealPath =
|
|
236
|
+
"/Users/user/.nvm/versions/node/v18.17.0/lib/node_modules/xiaozhi-client/dist/cli.js";
|
|
237
|
+
|
|
238
|
+
process.argv = ["node", npmBinPath];
|
|
239
|
+
mockRealpathSync.mockReturnValue(npmRealPath);
|
|
240
|
+
|
|
241
|
+
const webServerPath = PathUtils.getExecutablePath("WebServerLauncher");
|
|
242
|
+
|
|
243
|
+
const expectedDir =
|
|
244
|
+
"/Users/user/.nvm/versions/node/v18.17.0/lib/node_modules/xiaozhi-client/dist";
|
|
245
|
+
|
|
246
|
+
expect(webServerPath).toBe(
|
|
247
|
+
path.join(expectedDir, "WebServerLauncher.js")
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("应该在 Unix 系统路径格式下正确工作", () => {
|
|
252
|
+
const symlinkPath = "/usr/local/bin/xiaozhi";
|
|
253
|
+
const realPath = "/opt/xiaozhi-client/dist/cli.js";
|
|
254
|
+
const expectedDir = "/opt/xiaozhi-client/dist";
|
|
255
|
+
|
|
256
|
+
process.argv = ["node", symlinkPath];
|
|
257
|
+
mockRealpathSync.mockReturnValue(realPath);
|
|
258
|
+
|
|
259
|
+
const result = PathUtils.getExecutablePath("test");
|
|
260
|
+
const expected = path.join(expectedDir, "test.js");
|
|
261
|
+
|
|
262
|
+
expect(result).toBe(expected);
|
|
263
|
+
expect(mockRealpathSync).toHaveBeenCalledWith(symlinkPath);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("应该处理跨平台路径格式", () => {
|
|
267
|
+
// 测试不同平台的路径格式都能正确处理
|
|
268
|
+
const testCases = [
|
|
269
|
+
{
|
|
270
|
+
name: "Unix 风格路径",
|
|
271
|
+
symlinkPath: "/usr/local/bin/xiaozhi",
|
|
272
|
+
realPath: "/opt/xiaozhi-client/dist/cli.js",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "Windows 风格路径(使用正斜杠)",
|
|
276
|
+
symlinkPath: "C:/npm/xiaozhi",
|
|
277
|
+
realPath: "C:/npm/node_modules/xiaozhi-client/dist/cli.js",
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
for (const { name, symlinkPath, realPath } of testCases) {
|
|
282
|
+
process.argv = ["node", symlinkPath];
|
|
283
|
+
mockRealpathSync.mockReturnValue(realPath);
|
|
284
|
+
|
|
285
|
+
const result = PathUtils.getExecutablePath("test");
|
|
286
|
+
const expectedDir = path.dirname(realPath);
|
|
287
|
+
const expected = path.join(expectedDir, "test.js");
|
|
288
|
+
|
|
289
|
+
expect(result).toBe(expected);
|
|
290
|
+
expect(mockRealpathSync).toHaveBeenCalledWith(symlinkPath);
|
|
291
|
+
|
|
292
|
+
// 重置 mock 为下一次测试
|
|
293
|
+
mockRealpathSync.mockReset();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("应该确保路径解析的一致性", () => {
|
|
298
|
+
const symlinkPath = "/usr/bin/xiaozhi";
|
|
299
|
+
const realPath = "/home/user/xiaozhi-client/dist/cli.js";
|
|
300
|
+
|
|
301
|
+
process.argv = ["node", symlinkPath];
|
|
302
|
+
mockRealpathSync.mockReturnValue(realPath);
|
|
303
|
+
|
|
304
|
+
// 多次调用应该返回一致的结果
|
|
305
|
+
const results = [];
|
|
306
|
+
for (let i = 0; i < 5; i++) {
|
|
307
|
+
results.push(PathUtils.getExecutablePath("WebServerLauncher"));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const firstResult = results[0];
|
|
311
|
+
for (const result of results) {
|
|
312
|
+
expect(result).toBe(firstResult);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
expect(mockRealpathSync).toHaveBeenCalledTimes(5);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("错误恢复测试", () => {
|
|
320
|
+
it("应该处理各种文件系统错误", () => {
|
|
321
|
+
const errorCases = [
|
|
322
|
+
new Error("ENOENT: no such file or directory"),
|
|
323
|
+
new Error("EACCES: permission denied"),
|
|
324
|
+
new Error("ELOOP: too many symbolic links encountered"),
|
|
325
|
+
new Error("ENAMETOOLONG: name too long"),
|
|
326
|
+
new Error("ENOTDIR: not a directory"),
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
for (const error of errorCases) {
|
|
330
|
+
process.argv = ["node", "/test/path/cli.js"];
|
|
331
|
+
mockRealpathSync.mockImplementation(() => {
|
|
332
|
+
throw error;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const result = PathUtils.getExecutablePath("test");
|
|
336
|
+
const expected = path.join("/test/path", "test.js");
|
|
337
|
+
|
|
338
|
+
expect(result).toBe(expected);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("应该在符号链接循环时回退到原路径", () => {
|
|
343
|
+
process.argv = ["node", "/circular/symlink/xiaozhi"];
|
|
344
|
+
mockRealpathSync.mockImplementation(() => {
|
|
345
|
+
throw new Error("ELOOP: too many symbolic links encountered");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const result = PathUtils.getExecutablePath("WebServerLauncher");
|
|
349
|
+
const expected = path.join("/circular/symlink", "WebServerLauncher.js");
|
|
350
|
+
|
|
351
|
+
expect(result).toBe(expected);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("getWebServerLauncherPath 获取 WebServer 启动器路径", () => {
|
|
357
|
+
it("应该返回正确的 WebServerLauncher 路径", () => {
|
|
358
|
+
process.argv = ["node", "/Users/test/xiaozhi-client/dist/cli.js"];
|
|
359
|
+
|
|
360
|
+
const result = PathUtils.getWebServerLauncherPath();
|
|
361
|
+
const expected = path.join(
|
|
362
|
+
"/Users/test/xiaozhi-client/dist",
|
|
363
|
+
"WebServerLauncher.js"
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
expect(result).toBe(expected);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("路径解析一致性", () => {
|
|
371
|
+
it("应该在不同方法间保持一致的路径解析", () => {
|
|
372
|
+
// 使用跨平台的路径格式
|
|
373
|
+
const testCliPath = path.join("/test", "dist", "cli.js");
|
|
374
|
+
process.argv = ["node", testCliPath];
|
|
375
|
+
|
|
376
|
+
const webServerPath = PathUtils.getWebServerLauncherPath();
|
|
377
|
+
const customPath = PathUtils.getExecutablePath("custom");
|
|
378
|
+
|
|
379
|
+
// All paths should be in the same directory
|
|
380
|
+
const webServerDir = path.dirname(webServerPath);
|
|
381
|
+
const customDir = path.dirname(customPath);
|
|
382
|
+
|
|
383
|
+
expect(webServerDir).toBe(customDir);
|
|
384
|
+
// 使用跨平台的期望路径
|
|
385
|
+
const expectedDir = path.join("/test", "dist");
|
|
386
|
+
expect(customDir).toBe(expectedDir);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("边界情况", () => {
|
|
391
|
+
it("应该处理空的可执行文件名", () => {
|
|
392
|
+
process.argv = ["node", "/test/dist/cli.js"];
|
|
393
|
+
|
|
394
|
+
const result = PathUtils.getExecutablePath("");
|
|
395
|
+
const expected = path.join("/test/dist", ".js");
|
|
396
|
+
|
|
397
|
+
expect(result).toBe(expected);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("应该处理带扩展名的可执行文件名", () => {
|
|
401
|
+
process.argv = ["node", "/test/dist/cli.js"];
|
|
402
|
+
|
|
403
|
+
const result = PathUtils.getExecutablePath("test.exe");
|
|
404
|
+
const expected = path.join("/test/dist", "test.exe.js");
|
|
405
|
+
|
|
406
|
+
expect(result).toBe(expected);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("应该处理复杂的目录结构", () => {
|
|
410
|
+
process.argv = [
|
|
411
|
+
"node",
|
|
412
|
+
"/very/deep/nested/directory/structure/dist/cli.js",
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
const result = PathUtils.getExecutablePath("app");
|
|
416
|
+
const expected = path.join(
|
|
417
|
+
"/very/deep/nested/directory/structure/dist",
|
|
418
|
+
"app.js"
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
expect(result).toBe(expected);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { PathUtils } from "../PathUtils.js";
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
vi.mock("../FileUtils.js", () => ({
|
|
9
|
+
FileUtils: {
|
|
10
|
+
exists: vi.fn(),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("node:os", () => ({
|
|
15
|
+
tmpdir: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("node:url", () => ({
|
|
19
|
+
fileURLToPath: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock environment variables
|
|
23
|
+
const originalEnv = process.env;
|
|
24
|
+
|
|
25
|
+
describe("PathUtils - 文件系统路径", () => {
|
|
26
|
+
let mockFileExists: any;
|
|
27
|
+
let mockTmpdir: any;
|
|
28
|
+
let mockFileURLToPath: any;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
|
|
33
|
+
// Setup mocks
|
|
34
|
+
const { FileUtils } = await import("../FileUtils.js");
|
|
35
|
+
mockFileExists = vi.mocked(FileUtils.exists);
|
|
36
|
+
mockTmpdir = vi.mocked(tmpdir);
|
|
37
|
+
mockFileURLToPath = vi.mocked(fileURLToPath);
|
|
38
|
+
|
|
39
|
+
// Default mock implementations
|
|
40
|
+
mockFileExists.mockReturnValue(true);
|
|
41
|
+
mockTmpdir.mockReturnValue("/tmp");
|
|
42
|
+
mockFileURLToPath.mockReturnValue("/test/src/cli/utils/PathUtils.js");
|
|
43
|
+
|
|
44
|
+
// Reset environment variables
|
|
45
|
+
process.env = { ...originalEnv };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
process.env = originalEnv;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("getPidFile 获取 PID 文件路径", () => {
|
|
53
|
+
it("应该使用环境变量中的配置目录", () => {
|
|
54
|
+
process.env.XIAOZHI_CONFIG_DIR = "/custom/config";
|
|
55
|
+
|
|
56
|
+
const result = PathUtils.getPidFile();
|
|
57
|
+
const expected = path.join("/custom/config", ".xiaozhi-mcp-service.pid");
|
|
58
|
+
|
|
59
|
+
expect(result).toBe(expected);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("应该在没有环境变量时使用当前工作目录", () => {
|
|
63
|
+
process.env.XIAOZHI_CONFIG_DIR = undefined;
|
|
64
|
+
const originalCwd = process.cwd();
|
|
65
|
+
|
|
66
|
+
const result = PathUtils.getPidFile();
|
|
67
|
+
const expected = path.join(originalCwd, ".xiaozhi-mcp-service.pid");
|
|
68
|
+
|
|
69
|
+
expect(result).toBe(expected);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("应该处理空的环境变量", () => {
|
|
73
|
+
process.env.XIAOZHI_CONFIG_DIR = "";
|
|
74
|
+
const originalCwd = process.cwd();
|
|
75
|
+
|
|
76
|
+
const result = PathUtils.getPidFile();
|
|
77
|
+
const expected = path.join(originalCwd, ".xiaozhi-mcp-service.pid");
|
|
78
|
+
|
|
79
|
+
expect(result).toBe(expected);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("getLogFile 获取日志文件路径", () => {
|
|
84
|
+
it("应该使用提供的项目目录", () => {
|
|
85
|
+
const projectDir = "/custom/project";
|
|
86
|
+
|
|
87
|
+
const result = PathUtils.getLogFile(projectDir);
|
|
88
|
+
const expected = path.join(projectDir, "xiaozhi.log");
|
|
89
|
+
|
|
90
|
+
expect(result).toBe(expected);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("应该在没有项目目录时使用当前工作目录", () => {
|
|
94
|
+
const originalCwd = process.cwd();
|
|
95
|
+
|
|
96
|
+
const result = PathUtils.getLogFile();
|
|
97
|
+
const expected = path.join(originalCwd, "xiaozhi.log");
|
|
98
|
+
|
|
99
|
+
expect(result).toBe(expected);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("应该处理空字符串项目目录", () => {
|
|
103
|
+
const originalCwd = process.cwd();
|
|
104
|
+
|
|
105
|
+
const result = PathUtils.getLogFile("");
|
|
106
|
+
const expected = path.join(originalCwd, "xiaozhi.log");
|
|
107
|
+
|
|
108
|
+
expect(result).toBe(expected);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("getTemplatesDir 获取模板目录路径", () => {
|
|
113
|
+
it("应该返回所有可能的模板目录路径", () => {
|
|
114
|
+
mockFileURLToPath.mockReturnValue("/test/src/cli/utils/PathUtils.js");
|
|
115
|
+
|
|
116
|
+
const result = PathUtils.getTemplatesDir();
|
|
117
|
+
|
|
118
|
+
expect(result).toHaveLength(3);
|
|
119
|
+
expect(result[0]).toContain("templates");
|
|
120
|
+
expect(result[1]).toContain("templates");
|
|
121
|
+
expect(result[2]).toContain("templates");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("应该处理不同的脚本位置", () => {
|
|
125
|
+
mockFileURLToPath.mockReturnValue(
|
|
126
|
+
"/different/path/cli/utils/PathUtils.js"
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const result = PathUtils.getTemplatesDir();
|
|
130
|
+
|
|
131
|
+
expect(result).toHaveLength(3);
|
|
132
|
+
expect(result[0]).toBe(
|
|
133
|
+
path.join("/different/path/cli/utils", "templates")
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("findTemplatesDir 查找模板目录", () => {
|
|
139
|
+
it("应该返回第一个存在的模板目录", () => {
|
|
140
|
+
mockFileExists
|
|
141
|
+
.mockReturnValueOnce(false) // 第一个不存在
|
|
142
|
+
.mockReturnValueOnce(true); // 第二个存在
|
|
143
|
+
|
|
144
|
+
const result = PathUtils.findTemplatesDir();
|
|
145
|
+
|
|
146
|
+
expect(result).not.toBeNull();
|
|
147
|
+
expect(mockFileExists).toHaveBeenCalledTimes(2);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("应该在没有找到模板目录时返回 null", () => {
|
|
151
|
+
mockFileExists.mockReturnValue(false);
|
|
152
|
+
|
|
153
|
+
const result = PathUtils.findTemplatesDir();
|
|
154
|
+
|
|
155
|
+
expect(result).toBeNull();
|
|
156
|
+
expect(mockFileExists).toHaveBeenCalledTimes(3);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("应该返回第一个匹配的目录", () => {
|
|
160
|
+
mockFileExists.mockReturnValue(true);
|
|
161
|
+
|
|
162
|
+
const result = PathUtils.findTemplatesDir();
|
|
163
|
+
|
|
164
|
+
expect(result).not.toBeNull();
|
|
165
|
+
expect(mockFileExists).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("getTemplatePath 获取模板路径", () => {
|
|
170
|
+
it("应该返回存在的模板路径", () => {
|
|
171
|
+
const templateName = "test-template";
|
|
172
|
+
mockFileExists
|
|
173
|
+
.mockReturnValueOnce(true) // findTemplatesDir 找到目录
|
|
174
|
+
.mockReturnValueOnce(true); // 模板文件存在
|
|
175
|
+
|
|
176
|
+
const result = PathUtils.getTemplatePath(templateName);
|
|
177
|
+
|
|
178
|
+
expect(result).not.toBeNull();
|
|
179
|
+
expect(result).toContain(templateName);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("应该在模板目录不存在时返回 null", () => {
|
|
183
|
+
mockFileExists.mockReturnValue(false);
|
|
184
|
+
|
|
185
|
+
const result = PathUtils.getTemplatePath("test-template");
|
|
186
|
+
|
|
187
|
+
expect(result).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("应该在模板文件不存在时返回 null", () => {
|
|
191
|
+
mockFileExists
|
|
192
|
+
.mockReturnValueOnce(true) // findTemplatesDir 找到目录
|
|
193
|
+
.mockReturnValueOnce(false); // 模板文件不存在
|
|
194
|
+
|
|
195
|
+
const result = PathUtils.getTemplatePath("non-existent-template");
|
|
196
|
+
|
|
197
|
+
expect(result).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("getTempDir 获取临时目录路径", () => {
|
|
202
|
+
it("应该返回系统临时目录", () => {
|
|
203
|
+
process.env.TMPDIR = undefined;
|
|
204
|
+
process.env.TEMP = undefined;
|
|
205
|
+
mockTmpdir.mockReturnValue("/system/tmp");
|
|
206
|
+
|
|
207
|
+
const result = PathUtils.getTempDir();
|
|
208
|
+
|
|
209
|
+
expect(result).toBe("/system/tmp");
|
|
210
|
+
expect(mockTmpdir).toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("应该优先使用 TMPDIR 环境变量", () => {
|
|
214
|
+
process.env.TMPDIR = "/custom/tmp";
|
|
215
|
+
|
|
216
|
+
const result = PathUtils.getTempDir();
|
|
217
|
+
|
|
218
|
+
expect(result).toBe("/custom/tmp");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("应该使用 TEMP 环境变量作为备选", () => {
|
|
222
|
+
process.env.TMPDIR = undefined;
|
|
223
|
+
process.env.TEMP = "/windows/temp";
|
|
224
|
+
|
|
225
|
+
const result = PathUtils.getTempDir();
|
|
226
|
+
|
|
227
|
+
expect(result).toBe("/windows/temp");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("应该在没有环境变量时使用系统默认", () => {
|
|
231
|
+
process.env.TMPDIR = undefined;
|
|
232
|
+
process.env.TEMP = undefined;
|
|
233
|
+
mockTmpdir.mockReturnValue("/default/tmp");
|
|
234
|
+
|
|
235
|
+
const result = PathUtils.getTempDir();
|
|
236
|
+
|
|
237
|
+
expect(result).toBe("/default/tmp");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("getHomeDir 获取用户主目录路径", () => {
|
|
242
|
+
it("应该返回 HOME 环境变量", () => {
|
|
243
|
+
process.env.HOME = "/home/user";
|
|
244
|
+
process.env.USERPROFILE = undefined;
|
|
245
|
+
|
|
246
|
+
const result = PathUtils.getHomeDir();
|
|
247
|
+
|
|
248
|
+
expect(result).toBe("/home/user");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("应该使用 USERPROFILE 作为备选", () => {
|
|
252
|
+
process.env.HOME = undefined;
|
|
253
|
+
process.env.USERPROFILE = "C:\\Users\\user";
|
|
254
|
+
|
|
255
|
+
const result = PathUtils.getHomeDir();
|
|
256
|
+
|
|
257
|
+
expect(result).toBe("C:\\Users\\user");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("应该在没有环境变量时返回空字符串", () => {
|
|
261
|
+
process.env.HOME = undefined;
|
|
262
|
+
process.env.USERPROFILE = undefined;
|
|
263
|
+
|
|
264
|
+
const result = PathUtils.getHomeDir();
|
|
265
|
+
|
|
266
|
+
expect(result).toBe("");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("应该优先使用 HOME 而不是 USERPROFILE", () => {
|
|
270
|
+
process.env.HOME = "/home/user";
|
|
271
|
+
process.env.USERPROFILE = "C:\\Users\\user";
|
|
272
|
+
|
|
273
|
+
const result = PathUtils.getHomeDir();
|
|
274
|
+
|
|
275
|
+
expect(result).toBe("/home/user");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|