@zjex/git-workflow 0.2.23 → 0.3.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/.github/workflows/deploy-docs.yml +68 -0
- package/.github/workflows/test.yml +53 -0
- package/.husky/pre-commit +19 -0
- package/README.md +74 -1013
- package/TESTING.md +436 -0
- package/dist/index.js +104 -14
- package/docs/.vitepress/cache/deps/_metadata.json +52 -0
- package/docs/.vitepress/cache/deps/chunk-2CLQ7TTZ.js +9719 -0
- package/docs/.vitepress/cache/deps/chunk-2CLQ7TTZ.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-LE5NDSFD.js +12824 -0
- package/docs/.vitepress/cache/deps/chunk-LE5NDSFD.js.map +7 -0
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4505 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +583 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1352 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1665 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1813 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
- package/docs/.vitepress/cache/deps/vue.js +347 -0
- package/docs/.vitepress/cache/deps/vue.js.map +7 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-2CLQ7TTZ.js +9719 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-2CLQ7TTZ.js.map +7 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-LE5NDSFD.js +12824 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-LE5NDSFD.js.map +7 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/package.json +3 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vue_devtools-api.js +4505 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vue_devtools-api.js.map +7 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_core.js +583 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_core.js.map +7 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_integrations_useFocusTrap.js +1352 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___mark__js_src_vanilla__js.js +1665 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___minisearch.js +1813 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___minisearch.js.map +7 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vue.js +347 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vue.js.map +7 -0
- package/docs/.vitepress/config.ts +167 -0
- package/docs/.vitepress/theme/custom.css +39 -0
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/README.md +82 -0
- package/docs/commands/branch.md +468 -0
- package/docs/commands/commit.md +554 -0
- package/docs/commands/config.md +346 -0
- package/docs/commands/index.md +312 -0
- package/docs/commands/interactive.md +384 -0
- package/docs/commands/release.md +300 -0
- package/docs/commands/stash.md +309 -0
- package/docs/commands/tag.md +278 -0
- package/docs/commands/update.md +347 -0
- package/docs/config/ai-config.md +160 -0
- package/docs/config/branch-config.md +133 -0
- package/docs/config/commit-config.md +185 -0
- package/docs/config/config-file.md +776 -0
- package/docs/config/examples.md +279 -0
- package/docs/config/index.md +478 -0
- package/docs/guide/ai-commit.md +576 -0
- package/docs/guide/basic-usage.md +522 -0
- package/docs/guide/best-practices.md +426 -0
- package/docs/guide/branch-management.md +712 -0
- package/docs/guide/getting-started.md +294 -0
- package/docs/guide/index.md +168 -0
- package/docs/guide/installation.md +449 -0
- package/docs/guide/release-management.md +744 -0
- package/docs/guide/stash-management.md +608 -0
- package/docs/guide/tag-management.md +614 -0
- package/docs/index.md +205 -0
- package/docs/public/favicon.svg +21 -0
- package/docs/public/hero-logo.svg +43 -0
- package/docs/public/logo.svg +20 -0
- package/package.json +19 -3
- package/scripts/publish.js +55 -8
- package/scripts/publish.sh +20 -2
- package/scripts/release.sh +20 -2
- package/scripts/update-test-count.js +55 -0
- package/src/ai-service.ts +101 -15
- package/src/commands/init.ts +18 -0
- package/src/commands/tag.ts +1 -1
- package/src/config.ts +1 -0
- package/tests/COVERAGE_REPORT.md +222 -0
- package/tests/QUICK_START.md +242 -0
- package/tests/README.md +119 -0
- package/tests/TEST_SUMMARY.md +330 -0
- package/tests/ai-service.test.ts +705 -0
- package/tests/branch.test.ts +255 -0
- package/tests/commit.test.ts +85 -0
- package/tests/config.test.ts +311 -0
- package/tests/help.test.ts +134 -0
- package/tests/init.test.ts +582 -0
- package/tests/release.test.ts +333 -0
- package/tests/setup.ts +21 -0
- package/tests/stash.test.ts +376 -0
- package/tests/tag.test.ts +396 -0
- package/tests/update-notifier.test.ts +384 -0
- package/tests/update.test.ts +402 -0
- package/tests/utils.test.ts +229 -0
- package/vitest.config.ts +22 -0
- package/zjex-logo.svg +22 -0
- package/zjex-optimized.svg +34 -0
- package/zjex.svg +1 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync, unlinkSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
// Mock 所有外部依赖
|
|
8
|
+
vi.mock("child_process", () => ({
|
|
9
|
+
execSync: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("fs", () => ({
|
|
13
|
+
existsSync: vi.fn(),
|
|
14
|
+
unlinkSync: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("os", () => ({
|
|
18
|
+
homedir: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("path", () => ({
|
|
22
|
+
join: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("ora", () => ({
|
|
26
|
+
default: vi.fn(() => ({
|
|
27
|
+
start: vi.fn().mockReturnThis(),
|
|
28
|
+
stop: vi.fn().mockReturnThis(),
|
|
29
|
+
succeed: vi.fn().mockReturnThis(),
|
|
30
|
+
fail: vi.fn().mockReturnThis(),
|
|
31
|
+
})),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("boxen", () => ({
|
|
35
|
+
default: vi.fn((content: string) => content),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("semver", () => ({
|
|
39
|
+
default: {
|
|
40
|
+
gte: vi.fn(),
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock("../src/utils.js", () => ({
|
|
45
|
+
colors: {
|
|
46
|
+
bold: (text: string) => text,
|
|
47
|
+
green: (text: string) => text,
|
|
48
|
+
yellow: (text: string) => text,
|
|
49
|
+
red: (text: string) => text,
|
|
50
|
+
dim: (text: string) => text,
|
|
51
|
+
cyan: (text: string) => text,
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
describe("Update 模块测试", () => {
|
|
56
|
+
const mockExecSync = vi.mocked(execSync);
|
|
57
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
58
|
+
const mockUnlinkSync = vi.mocked(unlinkSync);
|
|
59
|
+
const mockHomedir = vi.mocked(homedir);
|
|
60
|
+
const mockJoin = vi.mocked(join);
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
// Mock console methods
|
|
65
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
66
|
+
vi.spyOn(process, "exit").mockImplementation(() => {
|
|
67
|
+
throw new Error("process.exit called");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Default mocks
|
|
71
|
+
mockHomedir.mockReturnValue("/home/user");
|
|
72
|
+
mockJoin.mockReturnValue("/home/user/.gw-update-check");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
vi.restoreAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("版本检查", () => {
|
|
80
|
+
it("应该正确获取最新版本", async () => {
|
|
81
|
+
mockExecSync.mockReturnValueOnce("1.2.4\n");
|
|
82
|
+
|
|
83
|
+
const semver = await import("semver");
|
|
84
|
+
vi.mocked(semver.default.gte).mockReturnValue(true);
|
|
85
|
+
|
|
86
|
+
const { update } = await import("../src/commands/update.js");
|
|
87
|
+
|
|
88
|
+
await update("1.2.3");
|
|
89
|
+
|
|
90
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
91
|
+
"npm view @zjex/git-workflow version",
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
encoding: "utf-8",
|
|
94
|
+
timeout: 3000,
|
|
95
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("应该处理网络错误", async () => {
|
|
101
|
+
mockExecSync.mockImplementation(() => {
|
|
102
|
+
throw new Error("Network error");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const { update } = await import("../src/commands/update.js");
|
|
106
|
+
|
|
107
|
+
await update("1.2.3");
|
|
108
|
+
|
|
109
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("请检查网络连接后重试"));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("应该正确比较版本号", async () => {
|
|
113
|
+
mockExecSync
|
|
114
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw (isUsingVolta)
|
|
115
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
116
|
+
.mockReturnValueOnce("update success"); // npm install
|
|
117
|
+
|
|
118
|
+
const semver = await import("semver");
|
|
119
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
120
|
+
|
|
121
|
+
const { update } = await import("../src/commands/update.js");
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await update("1.2.3");
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// 期望 process.exit 被调用
|
|
127
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
expect(semver.default.gte).toHaveBeenCalledWith("1.2.3", "1.2.4");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("Volta 检测", () => {
|
|
135
|
+
it("应该正确检测 Volta 环境", async () => {
|
|
136
|
+
mockExecSync
|
|
137
|
+
.mockReturnValueOnce("/home/user/.volta/bin/gw\n") // which gw (isUsingVolta)
|
|
138
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
139
|
+
.mockReturnValueOnce("update success"); // volta install
|
|
140
|
+
|
|
141
|
+
const semver = await import("semver");
|
|
142
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
143
|
+
|
|
144
|
+
const { update } = await import("../src/commands/update.js");
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await update("1.2.3");
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// 期望 process.exit 被调用
|
|
150
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
154
|
+
"volta install @zjex/git-workflow@latest",
|
|
155
|
+
expect.any(Object)
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("应该正确检测非 Volta 环境", async () => {
|
|
160
|
+
mockExecSync
|
|
161
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
162
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n"); // which gw
|
|
163
|
+
|
|
164
|
+
const semver = await import("semver");
|
|
165
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
166
|
+
|
|
167
|
+
// Mock 更新命令
|
|
168
|
+
mockExecSync.mockReturnValueOnce("update success");
|
|
169
|
+
|
|
170
|
+
const { update } = await import("../src/commands/update.js");
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await update("1.2.3");
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// 期望 process.exit 被调用
|
|
176
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
180
|
+
"npm install -g @zjex/git-workflow@latest",
|
|
181
|
+
expect.any(Object)
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("应该处理 which 命令失败", async () => {
|
|
186
|
+
mockExecSync
|
|
187
|
+
.mockImplementationOnce(() => { // which gw (isUsingVolta)
|
|
188
|
+
throw new Error("Command not found");
|
|
189
|
+
})
|
|
190
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
191
|
+
.mockReturnValueOnce("update success"); // npm install
|
|
192
|
+
|
|
193
|
+
const semver = await import("semver");
|
|
194
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
195
|
+
|
|
196
|
+
const { update } = await import("../src/commands/update.js");
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await update("1.2.3");
|
|
200
|
+
} catch (error) {
|
|
201
|
+
// 期望 process.exit 被调用
|
|
202
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 应该使用 npm 命令(默认)
|
|
206
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
207
|
+
"npm install -g @zjex/git-workflow@latest",
|
|
208
|
+
expect.any(Object)
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("更新流程", () => {
|
|
214
|
+
it("应该在已是最新版本时显示提示", async () => {
|
|
215
|
+
mockExecSync
|
|
216
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw (isUsingVolta)
|
|
217
|
+
.mockReturnValueOnce("1.2.3\n"); // npm view (same version)
|
|
218
|
+
|
|
219
|
+
const semver = await import("semver");
|
|
220
|
+
vi.mocked(semver.default.gte).mockReturnValue(true);
|
|
221
|
+
|
|
222
|
+
const { update } = await import("../src/commands/update.js");
|
|
223
|
+
|
|
224
|
+
await update("1.2.3");
|
|
225
|
+
|
|
226
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("✅ 已是最新版本"));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("应该成功执行更新", async () => {
|
|
230
|
+
mockExecSync
|
|
231
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
232
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw
|
|
233
|
+
.mockReturnValueOnce("update success"); // npm install
|
|
234
|
+
|
|
235
|
+
const semver = await import("semver");
|
|
236
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
237
|
+
|
|
238
|
+
const { update } = await import("../src/commands/update.js");
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await update("1.2.3");
|
|
242
|
+
} catch (error) {
|
|
243
|
+
// 期望 process.exit 被调用
|
|
244
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("🎉 发现新版本!"));
|
|
248
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("✨ 更新完成!"));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("应该处理更新失败", async () => {
|
|
252
|
+
mockExecSync
|
|
253
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
254
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw
|
|
255
|
+
.mockImplementationOnce(() => { // npm install
|
|
256
|
+
throw new Error("Update failed");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const semver = await import("semver");
|
|
260
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
261
|
+
|
|
262
|
+
const { update } = await import("../src/commands/update.js");
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await update("1.2.3");
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// 期望 process.exit 被调用
|
|
268
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("你可以手动运行以下命令更新"));
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("缓存管理", () => {
|
|
276
|
+
it("应该在更新成功后清理缓存", async () => {
|
|
277
|
+
mockExistsSync.mockReturnValue(true);
|
|
278
|
+
mockExecSync
|
|
279
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
280
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw
|
|
281
|
+
.mockReturnValueOnce("update success"); // npm install
|
|
282
|
+
|
|
283
|
+
const semver = await import("semver");
|
|
284
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
285
|
+
|
|
286
|
+
const { update } = await import("../src/commands/update.js");
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await update("1.2.3");
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// 期望 process.exit 被调用
|
|
292
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
expect(mockUnlinkSync).toHaveBeenCalledWith("/home/user/.gw-update-check");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("应该处理缓存文件不存在的情况", async () => {
|
|
299
|
+
mockExistsSync.mockReturnValue(false);
|
|
300
|
+
mockExecSync
|
|
301
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
302
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw
|
|
303
|
+
.mockReturnValueOnce("update success"); // npm install
|
|
304
|
+
|
|
305
|
+
const semver = await import("semver");
|
|
306
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
307
|
+
|
|
308
|
+
const { update } = await import("../src/commands/update.js");
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
await update("1.2.3");
|
|
312
|
+
} catch (error) {
|
|
313
|
+
// 期望 process.exit 被调用
|
|
314
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
expect(mockUnlinkSync).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("应该静默处理缓存清理错误", async () => {
|
|
321
|
+
mockExistsSync.mockReturnValue(true);
|
|
322
|
+
mockUnlinkSync.mockImplementation(() => {
|
|
323
|
+
throw new Error("Permission denied");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
mockExecSync
|
|
327
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
328
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw
|
|
329
|
+
.mockReturnValueOnce("update success"); // npm install
|
|
330
|
+
|
|
331
|
+
const semver = await import("semver");
|
|
332
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
333
|
+
|
|
334
|
+
const { update } = await import("../src/commands/update.js");
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
await update("1.2.3");
|
|
338
|
+
} catch (error) {
|
|
339
|
+
// 期望 process.exit 被调用,而不是缓存清理错误
|
|
340
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("用户界面", () => {
|
|
346
|
+
it("应该显示检查更新的提示", async () => {
|
|
347
|
+
mockExecSync.mockReturnValueOnce("1.2.3\n");
|
|
348
|
+
|
|
349
|
+
const semver = await import("semver");
|
|
350
|
+
vi.mocked(semver.default.gte).mockReturnValue(true);
|
|
351
|
+
|
|
352
|
+
const { update } = await import("../src/commands/update.js");
|
|
353
|
+
|
|
354
|
+
await update("1.2.3");
|
|
355
|
+
|
|
356
|
+
expect(console.log).toHaveBeenCalledWith("🔍 检查更新...");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("应该显示版本比较信息", async () => {
|
|
360
|
+
mockExecSync
|
|
361
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw (isUsingVolta)
|
|
362
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
363
|
+
.mockReturnValueOnce("update success"); // npm install
|
|
364
|
+
|
|
365
|
+
const semver = await import("semver");
|
|
366
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
367
|
+
|
|
368
|
+
const { update } = await import("../src/commands/update.js");
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await update("1.2.3");
|
|
372
|
+
} catch (error) {
|
|
373
|
+
// 期望 process.exit 被调用
|
|
374
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("1.2.3"));
|
|
378
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("1.2.4"));
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("应该显示验证命令提示", async () => {
|
|
382
|
+
mockExecSync
|
|
383
|
+
.mockReturnValueOnce("1.2.4\n") // npm view
|
|
384
|
+
.mockReturnValueOnce("/usr/local/bin/gw\n") // which gw
|
|
385
|
+
.mockReturnValueOnce("update success"); // npm install
|
|
386
|
+
|
|
387
|
+
const semver = await import("semver");
|
|
388
|
+
vi.mocked(semver.default.gte).mockReturnValue(false);
|
|
389
|
+
|
|
390
|
+
const { update } = await import("../src/commands/update.js");
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await update("1.2.3");
|
|
394
|
+
} catch (error) {
|
|
395
|
+
// 期望 process.exit 被调用
|
|
396
|
+
expect(error).toEqual(new Error("process.exit called"));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("hash -r && gw --version"));
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import {
|
|
4
|
+
colors,
|
|
5
|
+
TODAY,
|
|
6
|
+
theme,
|
|
7
|
+
exec,
|
|
8
|
+
execOutput,
|
|
9
|
+
checkGitRepo,
|
|
10
|
+
getMainBranch,
|
|
11
|
+
divider,
|
|
12
|
+
} from "../src/utils";
|
|
13
|
+
|
|
14
|
+
// Mock child_process
|
|
15
|
+
vi.mock("child_process");
|
|
16
|
+
|
|
17
|
+
describe("Utils 模块测试", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("colors 工具", () => {
|
|
27
|
+
it("应该正确添加红色", () => {
|
|
28
|
+
const result = colors.red("error");
|
|
29
|
+
expect(result).toContain("error");
|
|
30
|
+
expect(result).toMatch(/\x1b\[31m.*\x1b\[0m/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("应该正确添加绿色", () => {
|
|
34
|
+
const result = colors.green("success");
|
|
35
|
+
expect(result).toContain("success");
|
|
36
|
+
expect(result).toMatch(/\x1b\[32m.*\x1b\[0m/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("应该正确添加黄色", () => {
|
|
40
|
+
const result = colors.yellow("warning");
|
|
41
|
+
expect(result).toContain("warning");
|
|
42
|
+
expect(result).toMatch(/\x1b\[33m.*\x1b\[0m/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("应该正确添加青色", () => {
|
|
46
|
+
const result = colors.cyan("info");
|
|
47
|
+
expect(result).toContain("info");
|
|
48
|
+
expect(result).toMatch(/\x1b\[36m.*\x1b\[0m/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("应该正确添加暗淡效果", () => {
|
|
52
|
+
const result = colors.dim("dimmed");
|
|
53
|
+
expect(result).toContain("dimmed");
|
|
54
|
+
expect(result).toMatch(/\x1b\[2m.*\x1b\[0m/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("应该正确添加粗体", () => {
|
|
58
|
+
const result = colors.bold("bold");
|
|
59
|
+
expect(result).toContain("bold");
|
|
60
|
+
expect(result).toMatch(/\x1b\[1m.*\x1b\[0m/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("应该有 reset 代码", () => {
|
|
64
|
+
expect(colors.reset).toBe("\x1b[0m");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("TODAY 常量", () => {
|
|
69
|
+
it("应该是 YYYYMMDD 格式", () => {
|
|
70
|
+
expect(TODAY).toMatch(/^\d{8}$/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("应该是今天的日期", () => {
|
|
74
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
75
|
+
expect(TODAY).toBe(today);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("exec 函数", () => {
|
|
80
|
+
it("应该执行命令并返回输出", () => {
|
|
81
|
+
const mockExecSync = vi.mocked(execSync);
|
|
82
|
+
mockExecSync.mockReturnValue("output" as any);
|
|
83
|
+
|
|
84
|
+
const result = exec("git status", true);
|
|
85
|
+
|
|
86
|
+
expect(result).toBe("output");
|
|
87
|
+
expect(mockExecSync).toHaveBeenCalledWith("git status", {
|
|
88
|
+
encoding: "utf-8",
|
|
89
|
+
stdio: "pipe",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("静默模式失败时应该返回空字符串", () => {
|
|
94
|
+
const mockExecSync = vi.mocked(execSync);
|
|
95
|
+
mockExecSync.mockImplementation(() => {
|
|
96
|
+
throw new Error("Command failed");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = exec("git status", true);
|
|
100
|
+
|
|
101
|
+
expect(result).toBe("");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("非静默模式失败时应该抛出错误", () => {
|
|
105
|
+
const mockExecSync = vi.mocked(execSync);
|
|
106
|
+
mockExecSync.mockImplementation(() => {
|
|
107
|
+
throw new Error("Command failed");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(() => exec("git status", false)).toThrow();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("execOutput 函数", () => {
|
|
115
|
+
it("应该执行命令并返回 trim 后的输出", () => {
|
|
116
|
+
const mockExecSync = vi.mocked(execSync);
|
|
117
|
+
mockExecSync.mockReturnValue(" output \n" as any);
|
|
118
|
+
|
|
119
|
+
const result = execOutput("git branch");
|
|
120
|
+
|
|
121
|
+
expect(result).toBe("output");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("失败时应该返回空字符串", () => {
|
|
125
|
+
const mockExecSync = vi.mocked(execSync);
|
|
126
|
+
mockExecSync.mockImplementation(() => {
|
|
127
|
+
throw new Error("Command failed");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = execOutput("git branch");
|
|
131
|
+
|
|
132
|
+
expect(result).toBe("");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("checkGitRepo 函数", () => {
|
|
137
|
+
it("在 git 仓库中应该正常执行", () => {
|
|
138
|
+
const mockExecSync = vi.mocked(execSync);
|
|
139
|
+
mockExecSync.mockReturnValue("true" as any);
|
|
140
|
+
|
|
141
|
+
expect(() => checkGitRepo()).not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("不在 git 仓库中应该退出", () => {
|
|
145
|
+
const mockExecSync = vi.mocked(execSync);
|
|
146
|
+
mockExecSync.mockImplementation(() => {
|
|
147
|
+
throw new Error("Not a git repository");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const mockExit = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
151
|
+
throw new Error("process.exit");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(() => checkGitRepo()).toThrow("process.exit");
|
|
155
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
156
|
+
|
|
157
|
+
mockExit.mockRestore();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("getMainBranch 函数", () => {
|
|
162
|
+
it("应该返回 origin/main 如果存在", () => {
|
|
163
|
+
const mockExecSync = vi.mocked(execSync);
|
|
164
|
+
mockExecSync.mockReturnValue(" origin/main\n origin/develop\n" as any);
|
|
165
|
+
|
|
166
|
+
const result = getMainBranch();
|
|
167
|
+
|
|
168
|
+
expect(result).toBe("origin/main");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("应该返回 origin/master 如果 main 不存在", () => {
|
|
172
|
+
const mockExecSync = vi.mocked(execSync);
|
|
173
|
+
mockExecSync.mockReturnValue(
|
|
174
|
+
" origin/master\n origin/develop\n" as any
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const result = getMainBranch();
|
|
178
|
+
|
|
179
|
+
expect(result).toBe("origin/master");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("都不存在时应该默认返回 origin/main", () => {
|
|
183
|
+
const mockExecSync = vi.mocked(execSync);
|
|
184
|
+
mockExecSync.mockReturnValue(" origin/develop\n" as any);
|
|
185
|
+
|
|
186
|
+
const result = getMainBranch();
|
|
187
|
+
|
|
188
|
+
expect(result).toBe("origin/main");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("divider 函数", () => {
|
|
193
|
+
it("应该输出分隔线", () => {
|
|
194
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
195
|
+
|
|
196
|
+
divider();
|
|
197
|
+
|
|
198
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
199
|
+
expect.stringContaining("─".repeat(40))
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
consoleSpy.mockRestore();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("theme 对象", () => {
|
|
207
|
+
it("应该有 helpMode 属性", () => {
|
|
208
|
+
expect(theme).toHaveProperty("helpMode");
|
|
209
|
+
expect(theme.helpMode).toBe("always");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("应该有 style.keysHelpTip 方法", () => {
|
|
213
|
+
expect(theme.style).toHaveProperty("keysHelpTip");
|
|
214
|
+
expect(typeof theme.style.keysHelpTip).toBe("function");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("keysHelpTip 应该格式化按键提示", () => {
|
|
218
|
+
const keys: [string, string][] = [
|
|
219
|
+
["↑↓", "选择"],
|
|
220
|
+
["space", "确认"],
|
|
221
|
+
];
|
|
222
|
+
const result = theme.style.keysHelpTip(keys);
|
|
223
|
+
|
|
224
|
+
expect(result).toContain("↑↓ 选择");
|
|
225
|
+
expect(result).toContain("space 确认");
|
|
226
|
+
expect(result).toContain("Ctrl+C quit");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
// 抑制测试中的console输出,保持测试输出清洁
|
|
8
|
+
silent: false,
|
|
9
|
+
setupFiles: ["./tests/setup.ts"],
|
|
10
|
+
coverage: {
|
|
11
|
+
provider: "v8",
|
|
12
|
+
reporter: ["text", "json", "html"],
|
|
13
|
+
exclude: [
|
|
14
|
+
"node_modules/",
|
|
15
|
+
"dist/",
|
|
16
|
+
"**/*.config.ts",
|
|
17
|
+
"scripts/",
|
|
18
|
+
"**/*.test.ts",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
package/zjex-logo.svg
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="100" height="100">
|
|
2
|
+
<!-- ZJEX 纯图标版本 - 修复裁切问题,确保内容居中 -->
|
|
3
|
+
|
|
4
|
+
<!-- 交叉图案 - 完美居中,适当缩小 -->
|
|
5
|
+
<g transform="translate(60, 60) scale(0.8) translate(-51, -50)">
|
|
6
|
+
<!-- 蓝色条纹 -->
|
|
7
|
+
<path d="M69.3 59.41l-9.89 9.89L51 77.73 30.46 98.26A6 6 0 0 1 22 89.83L42.56 69.3 51 60.88l9.9-9.9z" fill="#0059cc"/>
|
|
8
|
+
<path d="M102 24.28a6 6 0 0 1-1.75 4.21L77.72 51l-8.42-8.44 22.5-22.5a6 6 0 0 1 10.2 4.22z" fill="#0059cc"/>
|
|
9
|
+
|
|
10
|
+
<!-- 青色条纹 -->
|
|
11
|
+
<path d="M83.64 6a5.94 5.94 0 0 1-1.74 4.21L41.08 51l-8.42-8.42 9.9-9.89L51 24.24 73.48 1.75A6 6 0 0 1 83.64 6z" fill="#00dac7"/>
|
|
12
|
+
<path d="M32.66 59.41L12.13 79.93a6 6 0 0 1-8.42-8.42L24.24 51z" fill="#00dac7"/>
|
|
13
|
+
|
|
14
|
+
<!-- 浅蓝色条纹 -->
|
|
15
|
+
<path d="M51 24.24l-8.42 8.43-22.5-22.5A6 6 0 0 1 24.28 0a5.89 5.89 0 0 1 4.2 1.75z" fill="#00baec"/>
|
|
16
|
+
<path d="M98.25 79.93a5.94 5.94 0 0 1-8.42 0L51 41.09l8.42-8.42 9.9 9.89 8.4 8.44 20.53 20.51a6 6 0 0 1 0 8.42z" fill="#00baec"/>
|
|
17
|
+
|
|
18
|
+
<!-- 深蓝色条纹 -->
|
|
19
|
+
<path d="M51 60.88l-8.44 8.42L1.74 28.49a6 6 0 0 1 8.43-8.43l22.49 22.5L41.08 51z" fill="#00abd8"/>
|
|
20
|
+
<path d="M79.93 98.26a6 6 0 0 1-8.42 0L51 77.73l8.42-8.43 20.51 20.53a6 6 0 0 1 0 8.43z" fill="#00abd8"/>
|
|
21
|
+
</g>
|
|
22
|
+
</svg>
|