@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.
- package/README.md +98 -0
- package/package.json +31 -0
- package/project.json +75 -0
- package/src/Constants.ts +105 -0
- package/src/Container.ts +212 -0
- package/src/Types.ts +79 -0
- package/src/commands/CommandHandlerFactory.ts +98 -0
- package/src/commands/ConfigCommandHandler.ts +279 -0
- package/src/commands/EndpointCommandHandler.ts +158 -0
- package/src/commands/McpCommandHandler.ts +778 -0
- package/src/commands/ProjectCommandHandler.ts +254 -0
- package/src/commands/ServiceCommandHandler.ts +182 -0
- package/src/commands/__tests__/CommandHandlerFactory.test.ts +323 -0
- package/src/commands/__tests__/CommandRegistry.test.ts +287 -0
- package/src/commands/__tests__/ConfigCommandHandler.test.ts +844 -0
- package/src/commands/__tests__/EndpointCommandHandler.test.ts +426 -0
- package/src/commands/__tests__/McpCommandHandler.test.ts +753 -0
- package/src/commands/__tests__/ProjectCommandHandler.test.ts +230 -0
- package/src/commands/__tests__/ServiceCommands.integration.test.ts +408 -0
- package/src/commands/index.ts +351 -0
- package/src/errors/ErrorHandlers.ts +141 -0
- package/src/errors/ErrorMessages.ts +121 -0
- package/src/errors/__tests__/index.test.ts +186 -0
- package/src/errors/index.ts +163 -0
- package/src/global.d.ts +19 -0
- package/src/index.ts +53 -0
- package/src/interfaces/Command.ts +128 -0
- package/src/interfaces/CommandTypes.ts +95 -0
- package/src/interfaces/Config.ts +25 -0
- package/src/interfaces/Service.ts +99 -0
- package/src/services/DaemonManager.ts +318 -0
- package/src/services/ProcessManager.ts +235 -0
- package/src/services/ServiceManager.ts +319 -0
- package/src/services/TemplateManager.ts +382 -0
- package/src/services/__tests__/DaemonManager.test.ts +378 -0
- package/src/services/__tests__/DaemonMode.integration.test.ts +321 -0
- package/src/services/__tests__/ProcessManager.test.ts +296 -0
- package/src/services/__tests__/ServiceManager.test.ts +774 -0
- package/src/services/__tests__/TemplateManager.test.ts +337 -0
- package/src/types/backend.d.ts +48 -0
- package/src/utils/FileUtils.ts +320 -0
- package/src/utils/FormatUtils.ts +198 -0
- package/src/utils/PathUtils.ts +255 -0
- package/src/utils/PlatformUtils.ts +217 -0
- package/src/utils/Validation.ts +274 -0
- package/src/utils/VersionUtils.ts +141 -0
- package/src/utils/__tests__/FileUtils.test.ts +728 -0
- package/src/utils/__tests__/FormatUtils.test.ts +243 -0
- package/src/utils/__tests__/PathUtils.test.ts +1165 -0
- package/src/utils/__tests__/PlatformUtils.test.ts +723 -0
- package/src/utils/__tests__/Validation.test.ts +560 -0
- package/src/utils/__tests__/VersionUtils.test.ts +410 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +100 -0
- package/vitest.config.ts +97 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectCommandHandler 测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { IDIContainer } from "../../interfaces/Config";
|
|
7
|
+
import { ProjectCommandHandler } from "../ProjectCommandHandler";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 测试用的 ProjectCommandHandler 子类,用于访问受保护的方法
|
|
11
|
+
*/
|
|
12
|
+
class TestableProjectCommandHandler extends ProjectCommandHandler {
|
|
13
|
+
// 暴露受保护的方法供测试使用
|
|
14
|
+
public async testHandleCreate(
|
|
15
|
+
projectName: string,
|
|
16
|
+
options: any
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
return this.handleCreate(projectName, options);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Mock ora
|
|
23
|
+
vi.mock("ora", () => ({
|
|
24
|
+
default: vi.fn().mockImplementation((text) => ({
|
|
25
|
+
start: () => ({
|
|
26
|
+
succeed: (message: string) => {
|
|
27
|
+
console.log(`✅ ${message}`);
|
|
28
|
+
},
|
|
29
|
+
fail: (message: string) => {
|
|
30
|
+
console.log(`✖ ${message}`);
|
|
31
|
+
},
|
|
32
|
+
warn: (message: string) => {
|
|
33
|
+
console.log(`⚠ ${message}`);
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
})),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Mock chalk
|
|
40
|
+
vi.mock("chalk", () => ({
|
|
41
|
+
default: {
|
|
42
|
+
red: (text: string) => text,
|
|
43
|
+
yellow: (text: string) => text,
|
|
44
|
+
gray: (text: string) => text,
|
|
45
|
+
green: (text: string) => text,
|
|
46
|
+
cyan: (text: string) => text,
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// Mock services
|
|
51
|
+
const mockErrorHandler = {
|
|
52
|
+
handle: vi.fn(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const mockTemplateManager = {
|
|
56
|
+
getAvailableTemplates: vi.fn(),
|
|
57
|
+
validateTemplate: vi.fn(),
|
|
58
|
+
createProject: vi.fn(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const mockFileUtils = {
|
|
62
|
+
exists: vi.fn(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const mockFormatUtils = {
|
|
66
|
+
calculateSimilarity: vi.fn(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const mockContainer = {
|
|
70
|
+
get: vi.fn((name: string) => {
|
|
71
|
+
if (name === "errorHandler") {
|
|
72
|
+
return mockErrorHandler;
|
|
73
|
+
}
|
|
74
|
+
if (name === "templateManager") {
|
|
75
|
+
return mockTemplateManager;
|
|
76
|
+
}
|
|
77
|
+
if (name === "fileUtils") {
|
|
78
|
+
return mockFileUtils;
|
|
79
|
+
}
|
|
80
|
+
if (name === "formatUtils") {
|
|
81
|
+
return mockFormatUtils;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}),
|
|
85
|
+
} as unknown as IDIContainer;
|
|
86
|
+
|
|
87
|
+
describe("ProjectCommandHandler", () => {
|
|
88
|
+
let handler: TestableProjectCommandHandler;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
handler = new TestableProjectCommandHandler(mockContainer);
|
|
92
|
+
vi.clearAllMocks();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("基本属性", () => {
|
|
96
|
+
it("应该有正确的命令名称", () => {
|
|
97
|
+
expect(handler.name).toBe("create");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("应该有正确的命令描述", () => {
|
|
101
|
+
expect(handler.description).toBe("创建项目");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("应该有正确的选项配置", () => {
|
|
105
|
+
expect(handler.options).toEqual([
|
|
106
|
+
{
|
|
107
|
+
flags: "-t, --template <templateName>",
|
|
108
|
+
description: "使用指定模板创建项目",
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("execute 方法", () => {
|
|
115
|
+
it("应该验证参数数量", async () => {
|
|
116
|
+
// 测试参数不足的情况
|
|
117
|
+
await expect(handler.execute([], {})).rejects.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("应该处理参数数量不足的情况", async () => {
|
|
121
|
+
await expect(handler.execute([], {})).rejects.toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("应该正确传递参数给 handleCreate", async () => {
|
|
125
|
+
const projectName = "test-project";
|
|
126
|
+
const options = { template: "basic" };
|
|
127
|
+
|
|
128
|
+
// Mock 服务以避免依赖问题
|
|
129
|
+
mockFileUtils.exists.mockResolvedValue(false);
|
|
130
|
+
mockTemplateManager.getAvailableTemplates.mockResolvedValue(["basic"]);
|
|
131
|
+
mockTemplateManager.validateTemplate.mockResolvedValue(true);
|
|
132
|
+
mockTemplateManager.createProject.mockResolvedValue(undefined);
|
|
133
|
+
|
|
134
|
+
await handler.execute([projectName], options);
|
|
135
|
+
|
|
136
|
+
// 验证 templateManager.createProject 被调用
|
|
137
|
+
expect(mockTemplateManager.createProject).toHaveBeenCalledWith({
|
|
138
|
+
templateName: "basic",
|
|
139
|
+
targetPath: expect.any(String),
|
|
140
|
+
projectName: "test-project",
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("handleCreate 方法", () => {
|
|
146
|
+
it("应该创建基本项目(无模板)", async () => {
|
|
147
|
+
const projectName = "basic-project";
|
|
148
|
+
const options = {};
|
|
149
|
+
|
|
150
|
+
// Mock 服务
|
|
151
|
+
mockFileUtils.exists.mockResolvedValue(false);
|
|
152
|
+
mockTemplateManager.createProject.mockResolvedValue(undefined);
|
|
153
|
+
|
|
154
|
+
await handler.testHandleCreate(projectName, options);
|
|
155
|
+
|
|
156
|
+
expect(mockTemplateManager.createProject).toHaveBeenCalledWith({
|
|
157
|
+
templateName: null,
|
|
158
|
+
targetPath: expect.any(String),
|
|
159
|
+
projectName: "basic-project",
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("应该处理项目目录已存在的情况", async () => {
|
|
164
|
+
const projectName = "existing-project";
|
|
165
|
+
const options = {};
|
|
166
|
+
|
|
167
|
+
// Mock 服务:目录已存在
|
|
168
|
+
mockFileUtils.exists.mockResolvedValue(true);
|
|
169
|
+
|
|
170
|
+
await handler.testHandleCreate(projectName, options);
|
|
171
|
+
|
|
172
|
+
// 验证没有调用 createProject
|
|
173
|
+
expect(mockTemplateManager.createProject).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("应该处理使用模板创建项目", async () => {
|
|
177
|
+
const projectName = "template-project";
|
|
178
|
+
const options = { template: "react" };
|
|
179
|
+
|
|
180
|
+
// Mock 服务
|
|
181
|
+
mockFileUtils.exists.mockResolvedValue(false);
|
|
182
|
+
mockTemplateManager.getAvailableTemplates.mockResolvedValue(["react"]);
|
|
183
|
+
mockTemplateManager.validateTemplate.mockResolvedValue(true);
|
|
184
|
+
mockTemplateManager.createProject.mockResolvedValue(undefined);
|
|
185
|
+
|
|
186
|
+
await handler.testHandleCreate(projectName, options);
|
|
187
|
+
|
|
188
|
+
expect(mockTemplateManager.createProject).toHaveBeenCalledWith({
|
|
189
|
+
templateName: "react",
|
|
190
|
+
targetPath: expect.any(String),
|
|
191
|
+
projectName: "template-project",
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("应该处理模板不存在的情况", async () => {
|
|
196
|
+
const projectName = "invalid-template-project";
|
|
197
|
+
const options = { template: "non-existent" };
|
|
198
|
+
|
|
199
|
+
// Mock 服务
|
|
200
|
+
mockFileUtils.exists.mockResolvedValue(false);
|
|
201
|
+
mockTemplateManager.getAvailableTemplates.mockResolvedValue([
|
|
202
|
+
"react",
|
|
203
|
+
"vue",
|
|
204
|
+
]);
|
|
205
|
+
mockTemplateManager.validateTemplate.mockResolvedValue(false);
|
|
206
|
+
mockFormatUtils.calculateSimilarity.mockReturnValue(0.3);
|
|
207
|
+
|
|
208
|
+
await handler.testHandleCreate(projectName, options);
|
|
209
|
+
|
|
210
|
+
// 验证没有调用 createProject
|
|
211
|
+
expect(mockTemplateManager.createProject).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("应该处理模板验证错误", async () => {
|
|
215
|
+
const projectName = "error-project";
|
|
216
|
+
const options = { template: "react" };
|
|
217
|
+
const error = new Error("模板验证失败");
|
|
218
|
+
|
|
219
|
+
// Mock 服务
|
|
220
|
+
mockFileUtils.exists.mockResolvedValue(false);
|
|
221
|
+
mockTemplateManager.getAvailableTemplates.mockResolvedValue(["react"]);
|
|
222
|
+
mockTemplateManager.validateTemplate.mockRejectedValue(error);
|
|
223
|
+
|
|
224
|
+
await handler.testHandleCreate(projectName, options);
|
|
225
|
+
|
|
226
|
+
// 验证错误处理
|
|
227
|
+
expect(mockErrorHandler.handle).toHaveBeenCalledWith(error);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ServiceCommandHandler } from "../ServiceCommandHandler";
|
|
3
|
+
|
|
4
|
+
// Mock ServiceManager
|
|
5
|
+
const mockServiceManager = {
|
|
6
|
+
start: vi.fn(),
|
|
7
|
+
stop: vi.fn(),
|
|
8
|
+
restart: vi.fn(),
|
|
9
|
+
getStatus: vi.fn(),
|
|
10
|
+
attach: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
vi.mock("@services/ServiceManager.js", () => ({
|
|
14
|
+
ServiceManager: vi.fn().mockImplementation(() => mockServiceManager),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock ProcessManager
|
|
18
|
+
const mockProcessManager = {
|
|
19
|
+
isServiceRunning: vi.fn(),
|
|
20
|
+
getServiceStatus: vi.fn(),
|
|
21
|
+
savePidInfo: vi.fn(),
|
|
22
|
+
cleanupPidFile: vi.fn(),
|
|
23
|
+
stopService: vi.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
vi.mock("@services/ProcessManager.js", () => ({
|
|
27
|
+
ProcessManager: vi.fn().mockImplementation(() => mockProcessManager),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// Mock DaemonManager
|
|
31
|
+
const mockDaemonManager = {
|
|
32
|
+
attachToLogs: vi.fn(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
vi.mock("@services/DaemonManager.js", () => ({
|
|
36
|
+
DaemonManager: vi.fn().mockImplementation(() => mockDaemonManager),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Mock ErrorHandler
|
|
40
|
+
const mockErrorHandler = {
|
|
41
|
+
handle: vi.fn(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
vi.mock("@utils/ErrorHandler.js", () => ({
|
|
45
|
+
ErrorHandler: mockErrorHandler,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
describe("服务命令集成测试", () => {
|
|
49
|
+
let serviceCommandHandler: ServiceCommandHandler;
|
|
50
|
+
let mockContainer: any;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
|
|
55
|
+
// Create mock container
|
|
56
|
+
mockContainer = {
|
|
57
|
+
get: vi.fn((serviceName: string) => {
|
|
58
|
+
switch (serviceName) {
|
|
59
|
+
case "serviceManager":
|
|
60
|
+
return mockServiceManager;
|
|
61
|
+
case "daemonManager":
|
|
62
|
+
return mockDaemonManager;
|
|
63
|
+
case "errorHandler":
|
|
64
|
+
return mockErrorHandler;
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(`Unknown service: ${serviceName}`);
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
serviceCommandHandler = new ServiceCommandHandler(mockContainer);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
// Don't restore all mocks to preserve spy functionality
|
|
76
|
+
// vi.restoreAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("带 daemon 选项的 start 命令", () => {
|
|
80
|
+
it("应正确执行带 daemon 选项的 start 命令", async () => {
|
|
81
|
+
const options = {
|
|
82
|
+
daemon: true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// 获取 start 子命令并执行
|
|
86
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
87
|
+
(cmd) => cmd.name === "start"
|
|
88
|
+
);
|
|
89
|
+
expect(startSubcommand).toBeDefined();
|
|
90
|
+
|
|
91
|
+
await startSubcommand!.execute([], options);
|
|
92
|
+
|
|
93
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith(
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
daemon: true,
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("应执行带 daemon 选项的 start 命令", async () => {
|
|
101
|
+
const options = {
|
|
102
|
+
daemon: true,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// 获取 start 子命令并执行
|
|
106
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
107
|
+
(cmd) => cmd.name === "start"
|
|
108
|
+
);
|
|
109
|
+
expect(startSubcommand).toBeDefined();
|
|
110
|
+
|
|
111
|
+
await startSubcommand!.execute([], options);
|
|
112
|
+
|
|
113
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith(
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
daemon: true,
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("应处理短格式的 daemon 选项 (-d)", async () => {
|
|
121
|
+
const options = {
|
|
122
|
+
daemon: true, // Commander.js 会将 -d 解析为 daemon: true
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// 获取 start 子命令并执行
|
|
126
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
127
|
+
(cmd) => cmd.name === "start"
|
|
128
|
+
);
|
|
129
|
+
expect(startSubcommand).toBeDefined();
|
|
130
|
+
|
|
131
|
+
await startSubcommand!.execute([], options);
|
|
132
|
+
|
|
133
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
daemon: true,
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("应处理缺失 daemon 选项的情况(前台模式)", async () => {
|
|
141
|
+
const options = {};
|
|
142
|
+
|
|
143
|
+
// 获取 start 子命令并执行
|
|
144
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
145
|
+
(cmd) => cmd.name === "start"
|
|
146
|
+
);
|
|
147
|
+
expect(startSubcommand).toBeDefined();
|
|
148
|
+
|
|
149
|
+
await startSubcommand!.execute([], options);
|
|
150
|
+
|
|
151
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith(
|
|
152
|
+
expect.objectContaining({
|
|
153
|
+
daemon: false,
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("status 命令", () => {
|
|
160
|
+
it("应正确执行 status 命令", async () => {
|
|
161
|
+
const mockStatus = {
|
|
162
|
+
running: true,
|
|
163
|
+
pid: 12345,
|
|
164
|
+
mode: "daemon",
|
|
165
|
+
uptime: "2h 30m",
|
|
166
|
+
port: 3000,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
mockServiceManager.getStatus.mockResolvedValue(mockStatus);
|
|
170
|
+
|
|
171
|
+
// 获取 status 子命令并执行
|
|
172
|
+
const statusSubcommand = serviceCommandHandler.subcommands?.find(
|
|
173
|
+
(cmd) => cmd.name === "status"
|
|
174
|
+
);
|
|
175
|
+
expect(statusSubcommand).toBeDefined();
|
|
176
|
+
|
|
177
|
+
await statusSubcommand!.execute([], {});
|
|
178
|
+
|
|
179
|
+
expect(mockServiceManager.getStatus).toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("应处理服务未运行时的 status 命令", async () => {
|
|
183
|
+
const mockStatus = {
|
|
184
|
+
running: false,
|
|
185
|
+
pid: null,
|
|
186
|
+
mode: null,
|
|
187
|
+
uptime: null,
|
|
188
|
+
port: null,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
mockServiceManager.getStatus.mockResolvedValue(mockStatus);
|
|
192
|
+
|
|
193
|
+
// 获取 status 子命令并执行
|
|
194
|
+
const statusSubcommand = serviceCommandHandler.subcommands?.find(
|
|
195
|
+
(cmd) => cmd.name === "status"
|
|
196
|
+
);
|
|
197
|
+
expect(statusSubcommand).toBeDefined();
|
|
198
|
+
|
|
199
|
+
await statusSubcommand!.execute([], {});
|
|
200
|
+
|
|
201
|
+
expect(mockServiceManager.getStatus).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("stop 命令", () => {
|
|
206
|
+
it("应正确执行 stop 命令", async () => {
|
|
207
|
+
mockServiceManager.stop.mockResolvedValue(undefined);
|
|
208
|
+
|
|
209
|
+
// 获取 stop 子命令并执行
|
|
210
|
+
const stopSubcommand = serviceCommandHandler.subcommands?.find(
|
|
211
|
+
(cmd) => cmd.name === "stop"
|
|
212
|
+
);
|
|
213
|
+
expect(stopSubcommand).toBeDefined();
|
|
214
|
+
|
|
215
|
+
await stopSubcommand!.execute([], {});
|
|
216
|
+
|
|
217
|
+
expect(mockServiceManager.stop).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("应处理服务未运行时的 stop 命令", async () => {
|
|
221
|
+
const error = new Error("Service is not running");
|
|
222
|
+
mockServiceManager.stop.mockRejectedValue(error);
|
|
223
|
+
|
|
224
|
+
// 获取 stop 子命令并执行
|
|
225
|
+
const stopSubcommand = serviceCommandHandler.subcommands?.find(
|
|
226
|
+
(cmd) => cmd.name === "stop"
|
|
227
|
+
);
|
|
228
|
+
expect(stopSubcommand).toBeDefined();
|
|
229
|
+
|
|
230
|
+
await stopSubcommand!.execute([], {});
|
|
231
|
+
|
|
232
|
+
// 验证错误处理器被调用
|
|
233
|
+
expect(mockErrorHandler.handle).toHaveBeenCalledWith(error);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("restart 命令", () => {
|
|
238
|
+
it("应执行带 daemon 选项的 restart 命令", async () => {
|
|
239
|
+
const options = {
|
|
240
|
+
daemon: true,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
mockServiceManager.restart.mockResolvedValue(undefined);
|
|
244
|
+
|
|
245
|
+
// 获取 restart 子命令并执行
|
|
246
|
+
const restartSubcommand = serviceCommandHandler.subcommands?.find(
|
|
247
|
+
(cmd) => cmd.name === "restart"
|
|
248
|
+
);
|
|
249
|
+
expect(restartSubcommand).toBeDefined();
|
|
250
|
+
|
|
251
|
+
await restartSubcommand!.execute([], options);
|
|
252
|
+
|
|
253
|
+
expect(mockServiceManager.restart).toHaveBeenCalledWith(
|
|
254
|
+
expect.objectContaining({
|
|
255
|
+
daemon: true,
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("应执行不带 daemon 选项的 restart 命令", async () => {
|
|
261
|
+
const options = {};
|
|
262
|
+
|
|
263
|
+
mockServiceManager.restart.mockResolvedValue(undefined);
|
|
264
|
+
|
|
265
|
+
// 获取 restart 子命令并执行
|
|
266
|
+
const restartSubcommand = serviceCommandHandler.subcommands?.find(
|
|
267
|
+
(cmd) => cmd.name === "restart"
|
|
268
|
+
);
|
|
269
|
+
expect(restartSubcommand).toBeDefined();
|
|
270
|
+
|
|
271
|
+
await restartSubcommand!.execute([], options);
|
|
272
|
+
|
|
273
|
+
expect(mockServiceManager.restart).toHaveBeenCalledWith(
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
daemon: false,
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("attach 命令", () => {
|
|
282
|
+
it("应正确执行 attach 命令", async () => {
|
|
283
|
+
mockDaemonManager.attachToLogs.mockResolvedValue(undefined);
|
|
284
|
+
|
|
285
|
+
// 获取 attach 子命令并执行
|
|
286
|
+
const attachSubcommand = serviceCommandHandler.subcommands?.find(
|
|
287
|
+
(cmd) => cmd.name === "attach"
|
|
288
|
+
);
|
|
289
|
+
expect(attachSubcommand).toBeDefined();
|
|
290
|
+
|
|
291
|
+
await attachSubcommand!.execute([], {});
|
|
292
|
+
|
|
293
|
+
expect(mockDaemonManager.attachToLogs).toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("应处理服务未运行时的 attach 命令", async () => {
|
|
297
|
+
const error = new Error("No service running to attach to");
|
|
298
|
+
mockDaemonManager.attachToLogs.mockRejectedValue(error);
|
|
299
|
+
|
|
300
|
+
// 获取 attach 子命令并执行
|
|
301
|
+
const attachSubcommand = serviceCommandHandler.subcommands?.find(
|
|
302
|
+
(cmd) => cmd.name === "attach"
|
|
303
|
+
);
|
|
304
|
+
expect(attachSubcommand).toBeDefined();
|
|
305
|
+
|
|
306
|
+
await attachSubcommand!.execute([], {});
|
|
307
|
+
|
|
308
|
+
// 验证错误处理器被调用
|
|
309
|
+
expect(mockErrorHandler.handle).toHaveBeenCalledWith(error);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("选项解析和验证", () => {
|
|
314
|
+
it("应处理传统模式启动", async () => {
|
|
315
|
+
const options = {
|
|
316
|
+
daemon: false,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// 获取 start 子命令并执行
|
|
320
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
321
|
+
(cmd) => cmd.name === "start"
|
|
322
|
+
);
|
|
323
|
+
expect(startSubcommand).toBeDefined();
|
|
324
|
+
|
|
325
|
+
await startSubcommand!.execute([], options);
|
|
326
|
+
|
|
327
|
+
// 验证服务管理器的 start 方法被调用
|
|
328
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith({
|
|
329
|
+
daemon: false,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("应处理未知选项时使用传统模式", async () => {
|
|
334
|
+
const options = {
|
|
335
|
+
server: "invalid", // 这个选项现在会被忽略,使用传统模式
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// 获取 start 子命令并执行
|
|
339
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
340
|
+
(cmd) => cmd.name === "start"
|
|
341
|
+
);
|
|
342
|
+
expect(startSubcommand).toBeDefined();
|
|
343
|
+
|
|
344
|
+
await startSubcommand!.execute([], options);
|
|
345
|
+
|
|
346
|
+
// 验证服务管理器的 start 方法被调用(传统模式)
|
|
347
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith({
|
|
348
|
+
daemon: false,
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("应处理布尔选项的各种变体", async () => {
|
|
353
|
+
const options = {
|
|
354
|
+
daemon: true,
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// 获取 start 子命令并执行
|
|
358
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
359
|
+
(cmd) => cmd.name === "start"
|
|
360
|
+
);
|
|
361
|
+
expect(startSubcommand).toBeDefined();
|
|
362
|
+
|
|
363
|
+
await startSubcommand!.execute([], options);
|
|
364
|
+
|
|
365
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith(
|
|
366
|
+
expect.objectContaining({
|
|
367
|
+
daemon: true,
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("错误处理", () => {
|
|
374
|
+
it("应优雅地处理服务管理器错误", async () => {
|
|
375
|
+
const error = new Error("Service manager error");
|
|
376
|
+
mockServiceManager.start.mockRejectedValue(error);
|
|
377
|
+
|
|
378
|
+
// 获取 start 子命令并执行
|
|
379
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
380
|
+
(cmd) => cmd.name === "start"
|
|
381
|
+
);
|
|
382
|
+
expect(startSubcommand).toBeDefined();
|
|
383
|
+
|
|
384
|
+
await startSubcommand!.execute([], { daemon: true });
|
|
385
|
+
|
|
386
|
+
// 验证错误处理器被调用
|
|
387
|
+
expect(mockErrorHandler.handle).toHaveBeenCalledWith(error);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("应处理未知命令", async () => {
|
|
391
|
+
// 主命令的 execute 方法只显示帮助信息,不会抛出错误
|
|
392
|
+
await expect(
|
|
393
|
+
serviceCommandHandler.execute(["unknown"], {})
|
|
394
|
+
).resolves.not.toThrow();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("应处理缺失的必需选项", async () => {
|
|
398
|
+
// 获取 start 子命令并执行
|
|
399
|
+
const startSubcommand = serviceCommandHandler.subcommands?.find(
|
|
400
|
+
(cmd) => cmd.name === "start"
|
|
401
|
+
);
|
|
402
|
+
expect(startSubcommand).toBeDefined();
|
|
403
|
+
|
|
404
|
+
// start 命令不需要必需选项,应该正常执行
|
|
405
|
+
await expect(startSubcommand!.execute([], {})).resolves.not.toThrow();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
});
|