@zjex/git-workflow 0.4.0 → 0.4.2

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.
@@ -0,0 +1,461 @@
1
+ # 测试指南
2
+
3
+ 本指南介绍 git-workflow 的测试策略、框架使用和最佳实践。
4
+
5
+ ## 测试框架
6
+
7
+ 我们使用 [Vitest](https://vitest.dev/) 作为测试框架,它提供:
8
+
9
+ - ⚡️ 快速的测试执行
10
+ - 🔄 Watch 模式支持
11
+ - 📊 内置代码覆盖率
12
+ - 🎯 与 Jest 兼容的 API
13
+ - 🔧 TypeScript 原生支持
14
+
15
+ ## 运行测试
16
+
17
+ ### 基本命令
18
+
19
+ ```bash
20
+ # 运行所有测试
21
+ npm test
22
+
23
+ # 运行特定测试文件
24
+ npm test -- tests/commit.test.ts
25
+
26
+ # Watch 模式(开发时使用)
27
+ npm run test:watch
28
+
29
+ # 查看测试覆盖率
30
+ npm run test:coverage
31
+ ```
32
+
33
+ ### 测试输出
34
+
35
+ ```bash
36
+ ✓ tests/commit.test.ts (18 tests) 9ms
37
+ ✓ tests/branch.test.ts (45 tests) 7ms
38
+ ✓ tests/tag.test.ts (58 tests) 14ms
39
+ ...
40
+
41
+ Test Files 15 passed (15)
42
+ Tests 375 passed (375)
43
+ Start at 17:34:11
44
+ Duration 426ms
45
+ ```
46
+
47
+ ## 测试覆盖率
48
+
49
+ 当前测试覆盖率:**375 个测试用例**
50
+
51
+ 查看详细覆盖率报告:
52
+
53
+ ```bash
54
+ npm run test:coverage
55
+ ```
56
+
57
+ 覆盖率报告会生成在 `coverage/` 目录,可以在浏览器中打开 `coverage/index.html` 查看。
58
+
59
+ ## 测试结构
60
+
61
+ ### 测试文件组织
62
+
63
+ ```
64
+ tests/
65
+ ├── ai-service.test.ts # AI 服务测试
66
+ ├── branch.test.ts # 分支管理测试
67
+ ├── commit.test.ts # 提交管理测试
68
+ ├── tag.test.ts # 标签管理测试
69
+ ├── stash.test.ts # Stash 管理测试
70
+ ├── release.test.ts # 发布管理测试
71
+ ├── log.test.ts # 日志查看测试
72
+ ├── update.test.ts # 更新检查测试
73
+ ├── config.test.ts # 配置管理测试
74
+ ├── utils.test.ts # 工具函数测试
75
+ ├── commands.test.ts # 命令别名测试
76
+ ├── clean.test.ts # 清理命令测试
77
+ ├── commit-format.test.ts # 提交格式测试
78
+ ├── init.test.ts # 初始化测试
79
+ ├── update-notifier.test.ts # 更新通知测试
80
+ └── setup.ts # 测试配置
81
+ ```
82
+
83
+ ### 测试文件命名
84
+
85
+ - 测试文件以 `.test.ts` 结尾
86
+ - 文件名与被测试的源文件对应
87
+ - 例如:`src/config.ts` → `tests/config.test.ts`
88
+
89
+ ## 编写测试
90
+
91
+ ### 基本测试结构
92
+
93
+ ```typescript
94
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
95
+
96
+ describe("功能模块名称", () => {
97
+ beforeEach(() => {
98
+ // 每个测试前的准备工作
99
+ vi.clearAllMocks();
100
+ });
101
+
102
+ afterEach(() => {
103
+ // 每个测试后的清理工作
104
+ vi.restoreAllMocks();
105
+ });
106
+
107
+ describe("子功能", () => {
108
+ it("应该正确处理某个场景", () => {
109
+ // Arrange: 准备测试数据
110
+ const input = "test";
111
+
112
+ // Act: 执行被测试的功能
113
+ const result = someFunction(input);
114
+
115
+ // Assert: 验证结果
116
+ expect(result).toBe("expected");
117
+ });
118
+ });
119
+ });
120
+ ```
121
+
122
+ ### Mock 外部依赖
123
+
124
+ #### Mock Node.js 模块
125
+
126
+ ```typescript
127
+ vi.mock("child_process", () => ({
128
+ execSync: vi.fn(),
129
+ spawn: vi.fn(),
130
+ }));
131
+
132
+ // 使用 mock
133
+ import { execSync } from "child_process";
134
+ const mockExecSync = vi.mocked(execSync);
135
+ mockExecSync.mockReturnValue("output");
136
+ ```
137
+
138
+ #### Mock 第三方库
139
+
140
+ ```typescript
141
+ vi.mock("@inquirer/prompts", () => ({
142
+ select: vi.fn(),
143
+ input: vi.fn(),
144
+ confirm: vi.fn(),
145
+ }));
146
+
147
+ // 使用 mock
148
+ import { select } from "@inquirer/prompts";
149
+ vi.mocked(select).mockResolvedValue("option1");
150
+ ```
151
+
152
+ #### Mock 工具函数
153
+
154
+ ```typescript
155
+ vi.mock("../src/utils.js", () => ({
156
+ colors: {
157
+ yellow: (text: string) => text,
158
+ green: (text: string) => text,
159
+ red: (text: string) => text,
160
+ },
161
+ execOutput: vi.fn(),
162
+ }));
163
+ ```
164
+
165
+ ### 测试异步函数
166
+
167
+ ```typescript
168
+ it("应该正确处理异步操作", async () => {
169
+ const result = await asyncFunction();
170
+ expect(result).toBe("expected");
171
+ });
172
+ ```
173
+
174
+ ### 测试错误处理
175
+
176
+ ```typescript
177
+ it("应该抛出错误", () => {
178
+ expect(() => {
179
+ functionThatThrows();
180
+ }).toThrow("Error message");
181
+ });
182
+
183
+ it("应该处理异步错误", async () => {
184
+ await expect(asyncFunctionThatThrows()).rejects.toThrow("Error message");
185
+ });
186
+ ```
187
+
188
+ ### 测试用户交互
189
+
190
+ ```typescript
191
+ it("应该正确处理用户选择", async () => {
192
+ const { select } = await import("@inquirer/prompts");
193
+ vi.mocked(select).mockResolvedValue("option1");
194
+
195
+ await interactiveFunction();
196
+
197
+ expect(select).toHaveBeenCalledWith(
198
+ expect.objectContaining({
199
+ message: "选择选项:",
200
+ choices: expect.arrayContaining([
201
+ expect.objectContaining({ value: "option1" }),
202
+ ]),
203
+ })
204
+ );
205
+ });
206
+ ```
207
+
208
+ ## 测试最佳实践
209
+
210
+ ### 1. 测试命名
211
+
212
+ 使用清晰的描述性名称:
213
+
214
+ ```typescript
215
+ // ✅ 好的命名
216
+ it("应该在没有配置文件时返回默认配置", () => {});
217
+ it("应该正确解析带有 Story ID 的分支名", () => {});
218
+
219
+ // ❌ 不好的命名
220
+ it("测试配置", () => {});
221
+ it("test branch", () => {});
222
+ ```
223
+
224
+ ### 2. 一个测试一个断言
225
+
226
+ 尽量让每个测试只验证一个行为:
227
+
228
+ ```typescript
229
+ // ✅ 好的做法
230
+ it("应该返回正确的分支名", () => {
231
+ const result = getBranchName("feature", "PROJ-123", "login");
232
+ expect(result).toBe("feature/20260116-PROJ-123-login");
233
+ });
234
+
235
+ it("应该处理没有 Story ID 的情况", () => {
236
+ const result = getBranchName("feature", "", "login");
237
+ expect(result).toBe("feature/20260116-login");
238
+ });
239
+
240
+ // ❌ 不好的做法
241
+ it("测试分支名生成", () => {
242
+ expect(getBranchName("feature", "PROJ-123", "login")).toBe("...");
243
+ expect(getBranchName("feature", "", "login")).toBe("...");
244
+ expect(getBranchName("hotfix", "BUG-456", "fix")).toBe("...");
245
+ });
246
+ ```
247
+
248
+ ### 3. 使用 beforeEach 准备测试数据
249
+
250
+ ```typescript
251
+ describe("配置管理", () => {
252
+ let mockConfig: Config;
253
+
254
+ beforeEach(() => {
255
+ mockConfig = {
256
+ branch: { prefix: "feature" },
257
+ commit: { useAI: true },
258
+ };
259
+ });
260
+
261
+ it("应该正确读取配置", () => {
262
+ // 使用 mockConfig
263
+ });
264
+ });
265
+ ```
266
+
267
+ ### 4. 清理 Mock
268
+
269
+ ```typescript
270
+ beforeEach(() => {
271
+ vi.clearAllMocks(); // 清除调用记录
272
+ });
273
+
274
+ afterEach(() => {
275
+ vi.restoreAllMocks(); // 恢复原始实现
276
+ });
277
+ ```
278
+
279
+ ### 5. 测试边界情况
280
+
281
+ ```typescript
282
+ describe("版本号解析", () => {
283
+ it("应该处理标准版本号", () => {
284
+ expect(parseVersion("v1.2.3")).toEqual({ major: 1, minor: 2, patch: 3 });
285
+ });
286
+
287
+ it("应该处理没有 v 前缀的版本号", () => {
288
+ expect(parseVersion("1.2.3")).toEqual({ major: 1, minor: 2, patch: 3 });
289
+ });
290
+
291
+ it("应该处理预发布版本", () => {
292
+ expect(parseVersion("v1.2.3-alpha.1")).toEqual({
293
+ major: 1,
294
+ minor: 2,
295
+ patch: 3,
296
+ prerelease: "alpha.1",
297
+ });
298
+ });
299
+
300
+ it("应该处理无效版本号", () => {
301
+ expect(() => parseVersion("invalid")).toThrow();
302
+ });
303
+ });
304
+ ```
305
+
306
+ ## 测试示例
307
+
308
+ ### 示例 1: 测试配置读取
309
+
310
+ ```typescript
311
+ import { describe, it, expect, vi } from "vitest";
312
+ import { readConfig } from "../src/config.js";
313
+
314
+ vi.mock("fs", () => ({
315
+ existsSync: vi.fn(),
316
+ readFileSync: vi.fn(),
317
+ }));
318
+
319
+ describe("配置读取", () => {
320
+ it("应该读取全局配置", () => {
321
+ const { existsSync, readFileSync } = await import("fs");
322
+ vi.mocked(existsSync).mockReturnValue(true);
323
+ vi.mocked(readFileSync).mockReturnValue(
324
+ JSON.stringify({ branch: { prefix: "feat" } })
325
+ );
326
+
327
+ const config = readConfig();
328
+
329
+ expect(config.branch.prefix).toBe("feat");
330
+ });
331
+
332
+ it("应该在配置文件不存在时返回默认配置", () => {
333
+ const { existsSync } = await import("fs");
334
+ vi.mocked(existsSync).mockReturnValue(false);
335
+
336
+ const config = readConfig();
337
+
338
+ expect(config).toEqual(DEFAULT_CONFIG);
339
+ });
340
+ });
341
+ ```
342
+
343
+ ### 示例 2: 测试 Git 命令
344
+
345
+ ```typescript
346
+ import { describe, it, expect, vi } from "vitest";
347
+ import { execSync } from "child_process";
348
+
349
+ vi.mock("child_process", () => ({
350
+ execSync: vi.fn(),
351
+ }));
352
+
353
+ describe("分支创建", () => {
354
+ it("应该执行正确的 Git 命令", () => {
355
+ const mockExecSync = vi.mocked(execSync);
356
+
357
+ createBranch("feature/20260116-PROJ-123-login");
358
+
359
+ expect(mockExecSync).toHaveBeenCalledWith(
360
+ 'git checkout -b "feature/20260116-PROJ-123-login"',
361
+ { stdio: "pipe" }
362
+ );
363
+ });
364
+ });
365
+ ```
366
+
367
+ ### 示例 3: 测试 AI 服务
368
+
369
+ ```typescript
370
+ import { describe, it, expect, vi } from "vitest";
371
+ import { generateCommitMessage } from "../src/ai-service.js";
372
+
373
+ describe("AI 提交消息生成", () => {
374
+ it("应该生成符合规范的提交消息", async () => {
375
+ const diff = "diff --git a/file.js...";
376
+
377
+ const message = await generateCommitMessage(diff, {
378
+ provider: "github",
379
+ apiKey: "test-key",
380
+ });
381
+
382
+ expect(message).toMatch(/^(feat|fix|docs|style|refactor|test|chore)/);
383
+ });
384
+
385
+ it("应该处理 API 错误", async () => {
386
+ await expect(
387
+ generateCommitMessage("diff", { provider: "invalid" })
388
+ ).rejects.toThrow();
389
+ });
390
+ });
391
+ ```
392
+
393
+ ## 持续集成
394
+
395
+ 测试会在以下情况自动运行:
396
+
397
+ - 每次 push 到 GitHub
398
+ - 每次创建 Pull Request
399
+ - 发布新版本前
400
+
401
+ GitHub Actions 配置在 `.github/workflows/` 目录。
402
+
403
+ ## 调试测试
404
+
405
+ ### 使用 console.log
406
+
407
+ ```typescript
408
+ it("调试测试", () => {
409
+ const result = someFunction();
410
+ console.log("Result:", result);
411
+ expect(result).toBe("expected");
412
+ });
413
+ ```
414
+
415
+ ### 运行单个测试
416
+
417
+ ```bash
418
+ # 运行特定文件
419
+ npm test -- tests/commit.test.ts
420
+
421
+ # 运行特定测试(使用 .only)
422
+ it.only("只运行这个测试", () => {
423
+ // ...
424
+ });
425
+ ```
426
+
427
+ ### 跳过测试
428
+
429
+ ```typescript
430
+ it.skip("暂时跳过这个测试", () => {
431
+ // ...
432
+ });
433
+ ```
434
+
435
+ ## 相关资源
436
+
437
+ - [Vitest 文档](https://vitest.dev/)
438
+ - [测试覆盖率报告](https://github.com/iamzjt-front-end/git-workflow/blob/main/TEST_COVERAGE_SUMMARY.md)
439
+ - [测试说明](https://github.com/iamzjt-front-end/git-workflow/blob/main/TESTING.md)
440
+
441
+ ## 贡献测试
442
+
443
+ 在提交 PR 时,请确保:
444
+
445
+ 1. ✅ 所有测试通过
446
+ 2. ✅ 新功能有对应的测试
447
+ 3. ✅ 测试覆盖率不降低
448
+ 4. ✅ 测试命名清晰
449
+ 5. ✅ Mock 正确清理
450
+
451
+ 运行测试:
452
+
453
+ ```bash
454
+ npm test
455
+ ```
456
+
457
+ 查看覆盖率:
458
+
459
+ ```bash
460
+ npm run test:coverage
461
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjex/git-workflow",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "🚀 极简的 Git 工作流 CLI 工具,让分支管理和版本发布变得轻松愉快",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "test:ui": "vitest --ui",
19
19
  "test:coverage": "vitest --coverage",
20
20
  "test:update-readme": "node scripts/update-test-count.js",
21
- "changelog": "changelogen -o CHANGELOG.md",
21
+ "changelog": "node scripts/generate-changelog-manual.js",
22
+ "changelog:full": "node scripts/generate-changelog-manual.js",
22
23
  "version": "node scripts/version.js",
23
24
  "release": "./scripts/release.sh",
24
25
  "release:dry": "./scripts/release.sh --dry-run",
@@ -63,7 +64,6 @@
63
64
  "@types/semver": "^7.7.1",
64
65
  "@vitest/coverage-v8": "^4.0.16",
65
66
  "@vitest/ui": "^4.0.16",
66
- "changelogen": "^0.6.2",
67
67
  "husky": "^9.1.7",
68
68
  "tsup": "^8.5.1",
69
69
  "tsx": "^4.21.0",
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 手动生成 CHANGELOG.md
5
+ * 直接使用 git log,避免 changelogen 的编码问题
6
+ */
7
+
8
+ import { execSync } from "child_process";
9
+ import { writeFileSync } from "fs";
10
+
11
+ const args = process.argv.slice(2);
12
+
13
+ console.log(`📝 生成 CHANGELOG...`);
14
+
15
+ // 获取所有 tags
16
+ const tags = execSync("git tag -l --sort=-version:refname", {
17
+ encoding: "utf8",
18
+ env: { ...process.env, LANG: "zh_CN.UTF-8", LC_ALL: "zh_CN.UTF-8" },
19
+ })
20
+ .trim()
21
+ .split("\n");
22
+
23
+ let changelog = "# Changelog\n\n";
24
+
25
+ // 为每个版本生成变更日志
26
+ for (let i = 0; i < tags.length; i++) {
27
+ const currentTag = tags[i];
28
+ const previousTag = tags[i + 1];
29
+
30
+ if (!previousTag) break;
31
+
32
+ changelog += `## [${currentTag}](https://github.com/iamzjt-front-end/git-workflow/compare/${previousTag}...${currentTag}) (${getTagDate(
33
+ currentTag
34
+ )})\n\n`;
35
+
36
+ // 获取该版本的提交
37
+ const commits = execSync(
38
+ `git log ${previousTag}..${currentTag} --pretty=format:"%s|%h" --no-merges`,
39
+ {
40
+ encoding: "utf8",
41
+ env: { ...process.env, LANG: "zh_CN.UTF-8", LC_ALL: "zh_CN.UTF-8" },
42
+ }
43
+ )
44
+ .trim()
45
+ .split("\n")
46
+ .filter(Boolean);
47
+
48
+ // 按类型分组
49
+ const groups = {
50
+ "✨ Features": [],
51
+ "🐛 Bug Fixes": [],
52
+ "📖 Documentation": [],
53
+ "🎨 Styles": [],
54
+ "♻️ Refactors": [],
55
+ "⚡ Performance": [],
56
+ "✅ Tests": [],
57
+ "🔧 Chore": [],
58
+ "🤖 CI": [],
59
+ };
60
+
61
+ commits.forEach((commit) => {
62
+ const [message, hash] = commit.split("|");
63
+ const link = `([${hash}](https://github.com/iamzjt-front-end/git-workflow/commit/${hash}))`;
64
+
65
+ if (message.match(/^(feat|✨)/i)) {
66
+ groups["✨ Features"].push(
67
+ `- ${message.replace(/^(feat|✨)[:(]\w*\)?:?\s*/i, "")} ${link}`
68
+ );
69
+ } else if (message.match(/^(fix|🐛)/i)) {
70
+ groups["🐛 Bug Fixes"].push(
71
+ `- ${message.replace(/^(fix|🐛)[:(]\w*\)?:?\s*/i, "")} ${link}`
72
+ );
73
+ } else if (message.match(/^(docs|📖|📝)/i)) {
74
+ groups["📖 Documentation"].push(
75
+ `- ${message.replace(/^(docs|📖|📝)[:(]\w*\)?:?\s*/i, "")} ${link}`
76
+ );
77
+ } else if (message.match(/^(style|🎨)/i)) {
78
+ groups["🎨 Styles"].push(
79
+ `- ${message.replace(/^(style|🎨)[:(]\w*\)?:?\s*/i, "")} ${link}`
80
+ );
81
+ } else if (message.match(/^(refactor|♻️)/i)) {
82
+ groups["♻️ Refactors"].push(
83
+ `- ${message.replace(/^(refactor|♻️)[:(]\w*\)?:?\s*/i, "")} ${link}`
84
+ );
85
+ } else if (message.match(/^(perf|⚡)/i)) {
86
+ groups["⚡ Performance"].push(
87
+ `- ${message.replace(/^(perf|⚡)[:(]\w*\)?:?\s*/i, "")} ${link}`
88
+ );
89
+ } else if (message.match(/^(test|✅)/i)) {
90
+ groups["✅ Tests"].push(
91
+ `- ${message.replace(/^(test|✅)[:(]\w*\)?:?\s*/i, "")} ${link}`
92
+ );
93
+ } else if (message.match(/^(chore|🔧|🏡)/i)) {
94
+ groups["🔧 Chore"].push(
95
+ `- ${message.replace(/^(chore|🔧|🏡)[:(]\w*\)?:?\s*/i, "")} ${link}`
96
+ );
97
+ } else if (message.match(/^(ci|🤖)/i)) {
98
+ groups["🤖 CI"].push(
99
+ `- ${message.replace(/^(ci|🤖)[:(]\w*\)?:?\s*/i, "")} ${link}`
100
+ );
101
+ } else {
102
+ groups["🔧 Chore"].push(`- ${message} ${link}`);
103
+ }
104
+ });
105
+
106
+ // 输出各分组
107
+ Object.entries(groups).forEach(([title, items]) => {
108
+ if (items.length > 0) {
109
+ changelog += `### ${title}\n\n`;
110
+ items.forEach((item) => {
111
+ changelog += `${item}\n`;
112
+ });
113
+ changelog += "\n";
114
+ }
115
+ });
116
+
117
+ changelog += "\n";
118
+ }
119
+
120
+ // 写入文件(添加 UTF-8 BOM 以确保编辑器正确识别编码)
121
+ const BOM = "\uFEFF";
122
+ writeFileSync("CHANGELOG.md", BOM + changelog, { encoding: "utf8" });
123
+ console.log("✅ CHANGELOG.md 生成成功!");
124
+
125
+ function getTagDate(tag) {
126
+ try {
127
+ const date = execSync(`git log -1 --format=%ai ${tag}`, {
128
+ encoding: "utf8",
129
+ env: { ...process.env, LANG: "zh_CN.UTF-8", LC_ALL: "zh_CN.UTF-8" },
130
+ }).trim();
131
+ return date.split(" ")[0];
132
+ } catch {
133
+ return new Date().toISOString().split("T")[0];
134
+ }
135
+ }