@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,296 @@
1
+ /**
2
+ * 进程管理服务单元测试
3
+ */
4
+
5
+ import { FileUtils } from "@cli/utils/FileUtils.js";
6
+ import { PathUtils } from "@cli/utils/PathUtils.js";
7
+ import { PlatformUtils } from "@cli/utils/PlatformUtils.js";
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import { ProcessManagerImpl } from "../ProcessManager";
10
+
11
+ // Mock 依赖
12
+ vi.mock("@cli/utils/FileUtils.js");
13
+ vi.mock("@cli/utils/PathUtils.js");
14
+ vi.mock("@cli/utils/PlatformUtils.js");
15
+
16
+ const mockFileUtils = vi.mocked(FileUtils);
17
+ const mockPathUtils = vi.mocked(PathUtils);
18
+ const mockPlatformUtils = vi.mocked(PlatformUtils);
19
+
20
+ describe("ProcessManagerImpl", () => {
21
+ let processManager: ProcessManagerImpl;
22
+ const mockPidFilePath = "/test/.xiaozhi-mcp-service.pid";
23
+
24
+ beforeEach(() => {
25
+ processManager = new ProcessManagerImpl();
26
+
27
+ // 重置所有 mock
28
+ vi.clearAllMocks();
29
+
30
+ // 设置默认 mock 返回值
31
+ mockPathUtils.getPidFile.mockReturnValue(mockPidFilePath);
32
+ mockPlatformUtils.isXiaozhiProcess.mockReturnValue(true);
33
+ mockPlatformUtils.processExists.mockReturnValue(true);
34
+ mockPlatformUtils.isContainerEnvironment.mockReturnValue(false);
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ });
40
+
41
+ describe("getServiceStatus", () => {
42
+ it("should return not running when PID file does not exist", () => {
43
+ mockFileUtils.exists.mockReturnValue(false);
44
+
45
+ const status = processManager.getServiceStatus();
46
+
47
+ expect(status.running).toBe(false);
48
+ expect(status.pid).toBeUndefined();
49
+ expect(status.uptime).toBeUndefined();
50
+ });
51
+
52
+ it("should return not running when PID file is corrupted", () => {
53
+ mockFileUtils.exists.mockReturnValue(true);
54
+ mockFileUtils.readFile.mockReturnValue("invalid-content");
55
+
56
+ const status = processManager.getServiceStatus();
57
+
58
+ expect(status.running).toBe(false);
59
+ expect(mockFileUtils.deleteFile).toHaveBeenCalledWith(mockPidFilePath);
60
+ });
61
+
62
+ it("should return not running when process is not xiaozhi process", () => {
63
+ mockFileUtils.exists.mockReturnValue(true);
64
+ mockFileUtils.readFile.mockReturnValue("1234|1640000000000|daemon");
65
+ mockPlatformUtils.isXiaozhiProcess.mockReturnValue(false);
66
+
67
+ const status = processManager.getServiceStatus();
68
+
69
+ expect(status.running).toBe(false);
70
+ expect(mockFileUtils.deleteFile).toHaveBeenCalledWith(mockPidFilePath);
71
+ });
72
+
73
+ it("should return running status when process is valid", () => {
74
+ const startTime = Date.now() - 60000; // 1 minute ago
75
+ mockFileUtils.exists.mockReturnValue(true);
76
+ mockFileUtils.readFile.mockReturnValue(`1234|${startTime}|daemon`);
77
+ mockPlatformUtils.isXiaozhiProcess.mockReturnValue(true);
78
+
79
+ const status = processManager.getServiceStatus();
80
+
81
+ expect(status.running).toBe(true);
82
+ expect(status.pid).toBe(1234);
83
+ expect(status.mode).toBe("daemon");
84
+ expect(status.uptime).toBeDefined();
85
+ });
86
+
87
+ it("should handle missing mode in PID file", () => {
88
+ const startTime = Date.now() - 60000;
89
+ mockFileUtils.exists.mockReturnValue(true);
90
+ mockFileUtils.readFile.mockReturnValue(`1234|${startTime}`);
91
+ mockPlatformUtils.isXiaozhiProcess.mockReturnValue(true);
92
+
93
+ const status = processManager.getServiceStatus();
94
+
95
+ expect(status.running).toBe(true);
96
+ expect(status.mode).toBe("foreground");
97
+ });
98
+ });
99
+
100
+ describe("savePidInfo", () => {
101
+ it("should save PID info to file", () => {
102
+ const pid = 1234;
103
+ const mode = "daemon";
104
+
105
+ processManager.savePidInfo(pid, mode);
106
+
107
+ expect(mockFileUtils.writeFile).toHaveBeenCalledWith(
108
+ mockPidFilePath,
109
+ expect.stringMatching(/^1234\|\d+\|daemon$/),
110
+ { overwrite: true }
111
+ );
112
+ });
113
+
114
+ it("should throw error when file write fails", () => {
115
+ mockFileUtils.writeFile.mockImplementation(() => {
116
+ throw new Error("Write failed");
117
+ });
118
+
119
+ expect(() => processManager.savePidInfo(1234, "daemon")).toThrow();
120
+ });
121
+ });
122
+
123
+ describe("killProcess", () => {
124
+ it("should kill process successfully", async () => {
125
+ mockPlatformUtils.killProcess.mockResolvedValue();
126
+
127
+ await processManager.killProcess(1234);
128
+
129
+ expect(mockPlatformUtils.killProcess).toHaveBeenCalledWith(1234);
130
+ });
131
+
132
+ it("should throw ProcessError when kill fails", async () => {
133
+ mockPlatformUtils.killProcess.mockRejectedValue(new Error("Kill failed"));
134
+
135
+ await expect(processManager.killProcess(1234)).rejects.toThrow(
136
+ "无法终止进程"
137
+ );
138
+ });
139
+ });
140
+
141
+ describe("gracefulKillProcess", () => {
142
+ let originalKill: typeof process.kill;
143
+
144
+ beforeEach(() => {
145
+ originalKill = process.kill;
146
+ process.kill = vi.fn();
147
+ });
148
+
149
+ afterEach(() => {
150
+ process.kill = originalKill;
151
+ });
152
+
153
+ it("should gracefully kill process", async () => {
154
+ let killCallCount = 0;
155
+ (process.kill as any).mockImplementation((pid: number, signal: any) => {
156
+ killCallCount++;
157
+ if (killCallCount > 1) {
158
+ throw new Error("ESRCH"); // Process stopped
159
+ }
160
+ });
161
+
162
+ await processManager.gracefulKillProcess(1234);
163
+
164
+ expect(process.kill).toHaveBeenCalledWith(1234, "SIGTERM");
165
+ });
166
+
167
+ it("should force kill if graceful kill fails", async () => {
168
+ (process.kill as any).mockImplementation((pid: number, signal: any) => {
169
+ if (signal === "SIGKILL") {
170
+ throw new Error("ESRCH"); // Process stopped
171
+ }
172
+ // Process still running for SIGTERM and signal 0
173
+ });
174
+
175
+ await processManager.gracefulKillProcess(1234);
176
+
177
+ expect(process.kill).toHaveBeenCalledWith(1234, "SIGTERM");
178
+ expect(process.kill).toHaveBeenCalledWith(1234, "SIGKILL");
179
+ });
180
+ });
181
+
182
+ describe("cleanupPidFile", () => {
183
+ it("should delete PID file if it exists", () => {
184
+ mockFileUtils.exists.mockReturnValue(true);
185
+
186
+ processManager.cleanupPidFile();
187
+
188
+ expect(mockFileUtils.deleteFile).toHaveBeenCalledWith(mockPidFilePath);
189
+ });
190
+
191
+ it("should not throw error if file does not exist", () => {
192
+ mockFileUtils.exists.mockReturnValue(false);
193
+
194
+ expect(() => processManager.cleanupPidFile()).not.toThrow();
195
+ });
196
+
197
+ it("should not throw error if delete fails", () => {
198
+ mockFileUtils.exists.mockReturnValue(true);
199
+ mockFileUtils.deleteFile.mockImplementation(() => {
200
+ throw new Error("Delete failed");
201
+ });
202
+
203
+ expect(() => processManager.cleanupPidFile()).not.toThrow();
204
+ });
205
+ });
206
+
207
+ describe("isXiaozhiProcess", () => {
208
+ it("should delegate to PlatformUtils", () => {
209
+ mockPlatformUtils.isXiaozhiProcess.mockReturnValue(true);
210
+
211
+ const result = processManager.isXiaozhiProcess(1234);
212
+
213
+ expect(result).toBe(true);
214
+ expect(mockPlatformUtils.isXiaozhiProcess).toHaveBeenCalledWith(1234);
215
+ });
216
+ });
217
+
218
+ describe("processExists", () => {
219
+ it("should delegate to PlatformUtils", () => {
220
+ mockPlatformUtils.processExists.mockReturnValue(true);
221
+
222
+ const result = processManager.processExists(1234);
223
+
224
+ expect(result).toBe(true);
225
+ expect(mockPlatformUtils.processExists).toHaveBeenCalledWith(1234);
226
+ });
227
+ });
228
+
229
+ describe("cleanupContainerState", () => {
230
+ it("should cleanup PID file in container environment", () => {
231
+ mockPlatformUtils.isContainerEnvironment.mockReturnValue(true);
232
+ mockFileUtils.exists.mockReturnValue(true);
233
+
234
+ processManager.cleanupContainerState();
235
+
236
+ expect(mockFileUtils.deleteFile).toHaveBeenCalledWith(mockPidFilePath);
237
+ });
238
+
239
+ it("should not cleanup in non-container environment", () => {
240
+ mockPlatformUtils.isContainerEnvironment.mockReturnValue(false);
241
+
242
+ processManager.cleanupContainerState();
243
+
244
+ expect(mockFileUtils.deleteFile).not.toHaveBeenCalled();
245
+ });
246
+ });
247
+
248
+ describe("getProcessInfo", () => {
249
+ it("should return process information", () => {
250
+ mockPlatformUtils.processExists.mockReturnValue(true);
251
+ mockPlatformUtils.isXiaozhiProcess.mockReturnValue(true);
252
+
253
+ const info = processManager.getProcessInfo(1234);
254
+
255
+ expect(info.exists).toBe(true);
256
+ expect(info.isXiaozhi).toBe(true);
257
+ });
258
+
259
+ it("should return false for non-existent process", () => {
260
+ mockPlatformUtils.processExists.mockReturnValue(false);
261
+
262
+ const info = processManager.getProcessInfo(1234);
263
+
264
+ expect(info.exists).toBe(false);
265
+ expect(info.isXiaozhi).toBe(false);
266
+ });
267
+ });
268
+
269
+ describe("validatePidFile", () => {
270
+ it("should return true for valid PID file", () => {
271
+ mockFileUtils.exists.mockReturnValue(true);
272
+ mockFileUtils.readFile.mockReturnValue("1234|1640000000000|daemon");
273
+
274
+ const isValid = processManager.validatePidFile();
275
+
276
+ expect(isValid).toBe(true);
277
+ });
278
+
279
+ it("should return false for invalid PID file", () => {
280
+ mockFileUtils.exists.mockReturnValue(true);
281
+ mockFileUtils.readFile.mockReturnValue("invalid");
282
+
283
+ const isValid = processManager.validatePidFile();
284
+
285
+ expect(isValid).toBe(false);
286
+ });
287
+
288
+ it("should return false when PID file does not exist", () => {
289
+ mockFileUtils.exists.mockReturnValue(false);
290
+
291
+ const isValid = processManager.validatePidFile();
292
+
293
+ expect(isValid).toBe(false);
294
+ });
295
+ });
296
+ });