@zjex/git-workflow 0.2.23 → 0.2.24

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,255 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { TODAY } from "../src/utils";
3
+
4
+ describe("Branch 功能测试", () => {
5
+ describe("分支命名规范", () => {
6
+ it("应该生成带 ID 的 feature 分支名", () => {
7
+ const prefix = "feature";
8
+ const id = "PROJ-123";
9
+ const description = "add-login";
10
+ const branchName = `${prefix}/${TODAY}-${id}-${description}`;
11
+
12
+ expect(branchName).toMatch(/^feature\/\d{8}-PROJ-123-add-login$/);
13
+ });
14
+
15
+ it("应该生成不带 ID 的 feature 分支名", () => {
16
+ const prefix = "feature";
17
+ const description = "add-login";
18
+ const branchName = `${prefix}/${TODAY}-${description}`;
19
+
20
+ expect(branchName).toMatch(/^feature\/\d{8}-add-login$/);
21
+ });
22
+
23
+ it("应该生成带 ID 的 hotfix 分支名", () => {
24
+ const prefix = "hotfix";
25
+ const id = "BUG-456";
26
+ const description = "fix-crash";
27
+ const branchName = `${prefix}/${TODAY}-${id}-${description}`;
28
+
29
+ expect(branchName).toMatch(/^hotfix\/\d{8}-BUG-456-fix-crash$/);
30
+ });
31
+
32
+ it("应该生成不带 ID 的 hotfix 分支名", () => {
33
+ const prefix = "hotfix";
34
+ const description = "fix-crash";
35
+ const branchName = `${prefix}/${TODAY}-${description}`;
36
+
37
+ expect(branchName).toMatch(/^hotfix\/\d{8}-fix-crash$/);
38
+ });
39
+
40
+ it("应该使用当前日期", () => {
41
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
42
+ expect(TODAY).toBe(today);
43
+ });
44
+ });
45
+
46
+ describe("分支前缀配置", () => {
47
+ it("应该支持自定义 feature 前缀", () => {
48
+ const customPrefix = "feat";
49
+ const description = "new-feature";
50
+ const branchName = `${customPrefix}/${TODAY}-${description}`;
51
+
52
+ expect(branchName).toMatch(/^feat\/\d{8}-new-feature$/);
53
+ });
54
+
55
+ it("应该支持自定义 hotfix 前缀", () => {
56
+ const customPrefix = "fix";
57
+ const description = "bug-fix";
58
+ const branchName = `${customPrefix}/${TODAY}-${description}`;
59
+
60
+ expect(branchName).toMatch(/^fix\/\d{8}-bug-fix$/);
61
+ });
62
+ });
63
+
64
+ describe("ID 验证", () => {
65
+ it("requireId 为 true 时 ID 不能为空", () => {
66
+ const requireId = true;
67
+ const id = "";
68
+
69
+ if (requireId && !id) {
70
+ expect(id).toBe("");
71
+ }
72
+ });
73
+
74
+ it("requireId 为 false 时 ID 可以为空", () => {
75
+ const requireId = false;
76
+ const id = "";
77
+
78
+ if (!requireId) {
79
+ expect(id).toBe("");
80
+ }
81
+ });
82
+
83
+ it("应该接受有效的 ID", () => {
84
+ const validIds = ["PROJ-123", "BUG-456", "TASK-789", "123", "ABC"];
85
+
86
+ validIds.forEach((id) => {
87
+ expect(id).toBeTruthy();
88
+ expect(id.length).toBeGreaterThan(0);
89
+ });
90
+ });
91
+ });
92
+
93
+ describe("描述验证", () => {
94
+ it("描述不能为空", () => {
95
+ const description = "";
96
+ expect(description).toBe("");
97
+ });
98
+
99
+ it("应该接受有效的描述", () => {
100
+ const validDescriptions = [
101
+ "add-login",
102
+ "fix-crash",
103
+ "update-readme",
104
+ "refactor-code",
105
+ ];
106
+
107
+ validDescriptions.forEach((desc) => {
108
+ expect(desc).toBeTruthy();
109
+ expect(desc.length).toBeGreaterThan(0);
110
+ });
111
+ });
112
+
113
+ it("描述应该使用连字符分隔", () => {
114
+ const description = "add-user-login";
115
+ expect(description).toContain("-");
116
+ expect(description.split("-").length).toBeGreaterThan(1);
117
+ });
118
+ });
119
+
120
+ describe("基础分支选择", () => {
121
+ it("应该支持指定基础分支", () => {
122
+ const baseBranch = "develop";
123
+ const fullBranch = `origin/${baseBranch}`;
124
+
125
+ expect(fullBranch).toBe("origin/develop");
126
+ });
127
+
128
+ it("应该支持 main 作为基础分支", () => {
129
+ const baseBranch = "main";
130
+ const fullBranch = `origin/${baseBranch}`;
131
+
132
+ expect(fullBranch).toBe("origin/main");
133
+ });
134
+
135
+ it("应该支持 master 作为基础分支", () => {
136
+ const baseBranch = "master";
137
+ const fullBranch = `origin/${baseBranch}`;
138
+
139
+ expect(fullBranch).toBe("origin/master");
140
+ });
141
+
142
+ it("应该支持自定义基础分支", () => {
143
+ const baseBranch = "release/1.0";
144
+ const fullBranch = `origin/${baseBranch}`;
145
+
146
+ expect(fullBranch).toBe("origin/release/1.0");
147
+ });
148
+ });
149
+
150
+ describe("分支名称格式", () => {
151
+ it("应该包含日期", () => {
152
+ const branchName = `feature/${TODAY}-add-feature`;
153
+ expect(branchName).toContain(TODAY);
154
+ });
155
+
156
+ it("应该包含前缀", () => {
157
+ const branchName = `feature/${TODAY}-add-feature`;
158
+ expect(branchName).toMatch(/^feature\//);
159
+ });
160
+
161
+ it("应该包含描述", () => {
162
+ const description = "add-feature";
163
+ const branchName = `feature/${TODAY}-${description}`;
164
+ expect(branchName).toContain(description);
165
+ });
166
+
167
+ it("带 ID 时应该包含 ID", () => {
168
+ const id = "PROJ-123";
169
+ const branchName = `feature/${TODAY}-${id}-add-feature`;
170
+ expect(branchName).toContain(id);
171
+ });
172
+
173
+ it("应该使用斜杠分隔前缀和名称", () => {
174
+ const branchName = `feature/${TODAY}-add-feature`;
175
+ expect(branchName).toContain("/");
176
+ const parts = branchName.split("/");
177
+ expect(parts.length).toBe(2);
178
+ expect(parts[0]).toBe("feature");
179
+ });
180
+ });
181
+
182
+ describe("分支类型", () => {
183
+ it("应该支持 feature 类型", () => {
184
+ const type: "feature" | "hotfix" = "feature";
185
+ expect(type).toBe("feature");
186
+ });
187
+
188
+ it("应该支持 hotfix 类型", () => {
189
+ const type: "feature" | "hotfix" = "hotfix";
190
+ expect(type).toBe("hotfix");
191
+ });
192
+ });
193
+
194
+ describe("autoPush 配置", () => {
195
+ it("autoPush 为 true 时应该自动推送", () => {
196
+ const autoPush = true;
197
+ expect(autoPush).toBe(true);
198
+ });
199
+
200
+ it("autoPush 为 false 时不应该推送", () => {
201
+ const autoPush = false;
202
+ expect(autoPush).toBe(false);
203
+ });
204
+
205
+ it("autoPush 未设置时应该询问", () => {
206
+ const autoPush = undefined;
207
+ expect(autoPush).toBeUndefined();
208
+ });
209
+ });
210
+
211
+ describe("分支名称边界情况", () => {
212
+ it("应该处理包含数字的描述", () => {
213
+ const description = "add-feature-v2";
214
+ const branchName = `feature/${TODAY}-${description}`;
215
+ expect(branchName).toContain("v2");
216
+ });
217
+
218
+ it("应该处理包含多个连字符的描述", () => {
219
+ const description = "add-user-login-feature";
220
+ const branchName = `feature/${TODAY}-${description}`;
221
+ expect(branchName.split("-").length).toBeGreaterThan(3);
222
+ });
223
+
224
+ it("应该处理长描述", () => {
225
+ const description = "add-very-long-feature-description-with-many-words";
226
+ const branchName = `feature/${TODAY}-${description}`;
227
+ expect(branchName.length).toBeGreaterThan(50);
228
+ });
229
+
230
+ it("应该处理包含下划线的 ID", () => {
231
+ const id = "PROJ_123";
232
+ const branchName = `feature/${TODAY}-${id}-add-feature`;
233
+ expect(branchName).toContain("PROJ_123");
234
+ });
235
+ });
236
+
237
+ describe("日期格式", () => {
238
+ it("TODAY 应该是 8 位数字", () => {
239
+ expect(TODAY).toMatch(/^\d{8}$/);
240
+ });
241
+
242
+ it("TODAY 应该是有效的日期格式", () => {
243
+ const year = parseInt(TODAY.slice(0, 4));
244
+ const month = parseInt(TODAY.slice(4, 6));
245
+ const day = parseInt(TODAY.slice(6, 8));
246
+
247
+ expect(year).toBeGreaterThan(2020);
248
+ expect(year).toBeLessThan(2100);
249
+ expect(month).toBeGreaterThanOrEqual(1);
250
+ expect(month).toBeLessThanOrEqual(12);
251
+ expect(day).toBeGreaterThanOrEqual(1);
252
+ expect(day).toBeLessThanOrEqual(31);
253
+ });
254
+ });
255
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ describe("Commit 功能测试", () => {
4
+ describe("提交类型", () => {
5
+ const commitTypes = [
6
+ { type: "feat", emoji: "✨", description: "新功能" },
7
+ { type: "fix", emoji: "🐛", description: "修复 Bug" },
8
+ { type: "docs", emoji: "📝", description: "文档更新" },
9
+ { type: "style", emoji: "💄", description: "代码格式(不影响功能)" },
10
+ {
11
+ type: "refactor",
12
+ emoji: "♻️",
13
+ description: "重构(既不是新功能也不是修复)",
14
+ },
15
+ { type: "perf", emoji: "⚡", description: "性能优化" },
16
+ { type: "test", emoji: "✅", description: "测试相关" },
17
+ { type: "build", emoji: "📦", description: "构建系统或外部依赖" },
18
+ { type: "ci", emoji: "👷", description: "CI 配置" },
19
+ { type: "chore", emoji: "🔧", description: "其他修改" },
20
+ ];
21
+
22
+ it("应该有 10 种提交类型", () => {
23
+ expect(commitTypes).toHaveLength(10);
24
+ });
25
+
26
+ it("每种类型都应该有必需的字段", () => {
27
+ commitTypes.forEach((type) => {
28
+ expect(type).toHaveProperty("type");
29
+ expect(type).toHaveProperty("emoji");
30
+ expect(type).toHaveProperty("description");
31
+ expect(type.type).toBeTruthy();
32
+ expect(type.emoji).toBeTruthy();
33
+ expect(type.description).toBeTruthy();
34
+ });
35
+ });
36
+
37
+ it("类型名称应该是小写", () => {
38
+ commitTypes.forEach((type) => {
39
+ expect(type.type).toBe(type.type.toLowerCase());
40
+ });
41
+ });
42
+ });
43
+
44
+ describe("提交消息格式", () => {
45
+ it("应该生成正确的提交消息格式", () => {
46
+ const type = "feat";
47
+ const emoji = "✨";
48
+ const scope = "tag";
49
+ const message = "支持多列显示";
50
+
51
+ const commitMessage = `${type}(${scope}): ${emoji} ${message}`;
52
+ expect(commitMessage).toBe("feat(tag): ✨ 支持多列显示");
53
+ });
54
+
55
+ it("无 scope 时应该省略括号", () => {
56
+ const type = "fix";
57
+ const emoji = "🐛";
58
+ const message = "修复对齐问题";
59
+
60
+ const commitMessage = `${type}: ${emoji} ${message}`;
61
+ expect(commitMessage).toBe("fix: 🐛 修复对齐问题");
62
+ });
63
+ });
64
+
65
+ describe("Refactor 对齐处理", () => {
66
+ it("refactor 类型应该添加额外空格", () => {
67
+ const type = "refactor";
68
+ const emoji = "♻️";
69
+ const extraSpace = type === "refactor" ? " " : "";
70
+ const display = `${emoji}${extraSpace} ${type}`;
71
+
72
+ // refactor 的 emoji 宽度不一致,需要额外空格
73
+ expect(display).toContain(" ");
74
+ });
75
+
76
+ it("其他类型不应该添加额外空格", () => {
77
+ const type = "feat";
78
+ const emoji = "✨";
79
+ const extraSpace = type === "refactor" ? " " : "";
80
+ const display = `${emoji}${extraSpace} ${type}`;
81
+
82
+ expect(extraSpace).toBe("");
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,311 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { loadConfig, getConfig } from "../src/config";
5
+ import { execOutput } from "../src/utils";
6
+
7
+ // Mock dependencies
8
+ vi.mock("fs");
9
+ vi.mock("os");
10
+ vi.mock("../src/utils");
11
+
12
+ describe("Config 模块测试", () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ // Mock homedir to return a valid path
16
+ vi.mocked(homedir).mockReturnValue("/home/user");
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ describe("默认配置", () => {
24
+ it("应该返回默认配置", () => {
25
+ vi.mocked(existsSync).mockReturnValue(false);
26
+ vi.mocked(execOutput).mockReturnValue("");
27
+
28
+ const config = loadConfig();
29
+
30
+ expect(config).toMatchObject({
31
+ featurePrefix: "feature",
32
+ hotfixPrefix: "hotfix",
33
+ requireId: false,
34
+ featureIdLabel: "Story ID",
35
+ hotfixIdLabel: "Issue ID",
36
+ autoStage: true,
37
+ useEmoji: true,
38
+ });
39
+ });
40
+ });
41
+
42
+ describe("项目配置", () => {
43
+ it("应该加载 .gwrc.json 配置", () => {
44
+ const mockConfig = {
45
+ featurePrefix: "feat",
46
+ requireId: true,
47
+ };
48
+
49
+ vi.mocked(existsSync).mockImplementation((path) => {
50
+ return path === ".gwrc.json";
51
+ });
52
+
53
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfig));
54
+ vi.mocked(execOutput).mockReturnValue("");
55
+
56
+ const config = loadConfig();
57
+
58
+ expect(config.featurePrefix).toBe("feat");
59
+ expect(config.requireId).toBe(true);
60
+ });
61
+
62
+ it("应该加载 .gwrc 配置", () => {
63
+ const mockConfig = {
64
+ hotfixPrefix: "fix",
65
+ };
66
+
67
+ vi.mocked(existsSync).mockImplementation((path) => {
68
+ return path === ".gwrc";
69
+ });
70
+
71
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfig));
72
+ vi.mocked(execOutput).mockReturnValue("");
73
+
74
+ const config = loadConfig();
75
+
76
+ expect(config.hotfixPrefix).toBe("fix");
77
+ });
78
+
79
+ it("应该加载 gw.config.json 配置", () => {
80
+ const mockConfig = {
81
+ autoPush: true,
82
+ };
83
+
84
+ vi.mocked(existsSync).mockImplementation((path) => {
85
+ return path === "gw.config.json";
86
+ });
87
+
88
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfig));
89
+ vi.mocked(execOutput).mockReturnValue("");
90
+
91
+ const config = loadConfig();
92
+
93
+ expect(config.autoPush).toBe(true);
94
+ });
95
+ });
96
+
97
+ describe("全局配置", () => {
98
+ it("应该加载全局配置", () => {
99
+ const mockGlobalConfig = {
100
+ aiCommit: {
101
+ enabled: true,
102
+ provider: "github" as const,
103
+ apiKey: "test-key",
104
+ },
105
+ };
106
+
107
+ vi.mocked(homedir).mockReturnValue("/home/user");
108
+ vi.mocked(existsSync).mockImplementation((path) => {
109
+ return path === "/home/user/.gwrc.json";
110
+ });
111
+
112
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockGlobalConfig));
113
+ vi.mocked(execOutput).mockReturnValue("");
114
+
115
+ const config = loadConfig();
116
+
117
+ expect(config.aiCommit?.enabled).toBe(true);
118
+ expect(config.aiCommit?.provider).toBe("github");
119
+ expect(config.aiCommit?.apiKey).toBe("test-key");
120
+ });
121
+ });
122
+
123
+ describe("配置合并", () => {
124
+ it("项目配置应该覆盖全局配置", () => {
125
+ const mockGlobalConfig = {
126
+ featurePrefix: "global-feature",
127
+ requireId: false,
128
+ };
129
+
130
+ const mockProjectConfig = {
131
+ featurePrefix: "project-feature",
132
+ };
133
+
134
+ vi.mocked(homedir).mockReturnValue("/home/user");
135
+ vi.mocked(existsSync).mockImplementation((path) => {
136
+ if (path === "/home/user/.gwrc.json") return true;
137
+ if (path === ".gwrc.json") return true;
138
+ return false;
139
+ });
140
+
141
+ vi.mocked(readFileSync).mockImplementation((path) => {
142
+ if (path === "/home/user/.gwrc.json") {
143
+ return JSON.stringify(mockGlobalConfig);
144
+ }
145
+ if (path === ".gwrc.json") {
146
+ return JSON.stringify(mockProjectConfig);
147
+ }
148
+ return "{}";
149
+ });
150
+
151
+ vi.mocked(execOutput).mockReturnValue("");
152
+
153
+ const config = loadConfig();
154
+
155
+ expect(config.featurePrefix).toBe("project-feature");
156
+ expect(config.requireId).toBe(false);
157
+ });
158
+
159
+ it("应该深度合并 aiCommit 配置", () => {
160
+ const mockGlobalConfig = {
161
+ aiCommit: {
162
+ enabled: true,
163
+ provider: "github" as const,
164
+ apiKey: "global-key",
165
+ language: "zh-CN" as const,
166
+ },
167
+ };
168
+
169
+ const mockProjectConfig = {
170
+ aiCommit: {
171
+ provider: "openai" as const,
172
+ model: "gpt-4",
173
+ },
174
+ };
175
+
176
+ vi.mocked(homedir).mockReturnValue("/home/user");
177
+ vi.mocked(existsSync).mockImplementation((path) => {
178
+ if (path === "/home/user/.gwrc.json") return true;
179
+ if (path === ".gwrc.json") return true;
180
+ return false;
181
+ });
182
+
183
+ vi.mocked(readFileSync).mockImplementation((path) => {
184
+ if (path === "/home/user/.gwrc.json") {
185
+ return JSON.stringify(mockGlobalConfig);
186
+ }
187
+ if (path === ".gwrc.json") {
188
+ return JSON.stringify(mockProjectConfig);
189
+ }
190
+ return "{}";
191
+ });
192
+
193
+ vi.mocked(execOutput).mockReturnValue("");
194
+
195
+ const config = loadConfig();
196
+
197
+ expect(config.aiCommit?.enabled).toBe(true);
198
+ expect(config.aiCommit?.provider).toBe("openai");
199
+ expect(config.aiCommit?.apiKey).toBe("global-key");
200
+ expect(config.aiCommit?.language).toBe("zh-CN");
201
+ expect(config.aiCommit?.model).toBe("gpt-4");
202
+ });
203
+ });
204
+
205
+ describe("配置文件解析错误", () => {
206
+ it("全局配置解析失败时应该使用默认配置", () => {
207
+ vi.mocked(homedir).mockReturnValue("/home/user");
208
+ vi.mocked(existsSync).mockImplementation((path) => {
209
+ return path === "/home/user/.gwrc.json";
210
+ });
211
+
212
+ vi.mocked(readFileSync).mockReturnValue("invalid json");
213
+ vi.mocked(execOutput).mockReturnValue("");
214
+
215
+ const consoleWarnSpy = vi
216
+ .spyOn(console, "warn")
217
+ .mockImplementation(() => {});
218
+
219
+ const config = loadConfig();
220
+
221
+ expect(config.featurePrefix).toBe("feature");
222
+ expect(consoleWarnSpy).toHaveBeenCalled();
223
+
224
+ consoleWarnSpy.mockRestore();
225
+ });
226
+
227
+ it("项目配置解析失败时应该使用默认配置", () => {
228
+ vi.mocked(existsSync).mockImplementation((path) => {
229
+ return path === ".gwrc.json";
230
+ });
231
+
232
+ vi.mocked(readFileSync).mockReturnValue("invalid json");
233
+ vi.mocked(execOutput).mockReturnValue("");
234
+
235
+ const consoleWarnSpy = vi
236
+ .spyOn(console, "warn")
237
+ .mockImplementation(() => {});
238
+
239
+ const config = loadConfig();
240
+
241
+ expect(config.featurePrefix).toBe("feature");
242
+ expect(consoleWarnSpy).toHaveBeenCalled();
243
+
244
+ consoleWarnSpy.mockRestore();
245
+ });
246
+ });
247
+
248
+ describe("getConfig 函数", () => {
249
+ it("应该缓存配置", () => {
250
+ vi.mocked(existsSync).mockReturnValue(false);
251
+ vi.mocked(execOutput).mockReturnValue("");
252
+
253
+ const config1 = getConfig();
254
+ const config2 = getConfig();
255
+
256
+ expect(config1).toBe(config2);
257
+ });
258
+ });
259
+
260
+ describe("Git 根目录查找", () => {
261
+ it("应该在 git 根目录查找配置", () => {
262
+ vi.mocked(execOutput).mockReturnValue("/path/to/repo");
263
+ vi.mocked(existsSync).mockImplementation((path) => {
264
+ return path === "/path/to/repo/.gwrc.json";
265
+ });
266
+
267
+ vi.mocked(readFileSync).mockReturnValue(
268
+ JSON.stringify({ featurePrefix: "git-root-feature" })
269
+ );
270
+
271
+ const config = loadConfig();
272
+
273
+ expect(config.featurePrefix).toBe("git-root-feature");
274
+ });
275
+ });
276
+
277
+ describe("配置优先级", () => {
278
+ it("当前目录 > git 根目录 > 全局配置", () => {
279
+ const mockGlobalConfig = { featurePrefix: "global" };
280
+ const mockGitRootConfig = { featurePrefix: "git-root" };
281
+ const mockCurrentConfig = { featurePrefix: "current" };
282
+
283
+ vi.mocked(homedir).mockReturnValue("/home/user");
284
+ vi.mocked(execOutput).mockReturnValue("/path/to/repo");
285
+
286
+ vi.mocked(existsSync).mockImplementation((path) => {
287
+ if (path === "/home/user/.gwrc.json") return true;
288
+ if (path === "/path/to/repo/.gwrc.json") return true;
289
+ if (path === ".gwrc.json") return true;
290
+ return false;
291
+ });
292
+
293
+ vi.mocked(readFileSync).mockImplementation((path) => {
294
+ if (path === "/home/user/.gwrc.json") {
295
+ return JSON.stringify(mockGlobalConfig);
296
+ }
297
+ if (path === "/path/to/repo/.gwrc.json") {
298
+ return JSON.stringify(mockGitRootConfig);
299
+ }
300
+ if (path === ".gwrc.json") {
301
+ return JSON.stringify(mockCurrentConfig);
302
+ }
303
+ return "{}";
304
+ });
305
+
306
+ const config = loadConfig();
307
+
308
+ expect(config.featurePrefix).toBe("current");
309
+ });
310
+ });
311
+ });