@zjex/git-workflow 0.2.24 → 0.3.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.
Files changed (99) hide show
  1. package/.github/workflows/deploy-docs.yml +68 -0
  2. package/.github/workflows/test.yml +24 -4
  3. package/.husky/pre-commit +17 -0
  4. package/README.md +72 -1066
  5. package/ROADMAP.md +275 -0
  6. package/dist/index.js +450 -99
  7. package/docs/.vitepress/cache/deps/_metadata.json +52 -0
  8. package/docs/.vitepress/cache/deps/chunk-2CLQ7TTZ.js +9719 -0
  9. package/docs/.vitepress/cache/deps/chunk-2CLQ7TTZ.js.map +7 -0
  10. package/docs/.vitepress/cache/deps/chunk-LE5NDSFD.js +12824 -0
  11. package/docs/.vitepress/cache/deps/chunk-LE5NDSFD.js.map +7 -0
  12. package/docs/.vitepress/cache/deps/package.json +3 -0
  13. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4505 -0
  14. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  15. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +583 -0
  16. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  17. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1352 -0
  18. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  19. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1665 -0
  20. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  21. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1813 -0
  22. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
  23. package/docs/.vitepress/cache/deps/vue.js +347 -0
  24. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  25. package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-2CLQ7TTZ.js +9719 -0
  26. package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-2CLQ7TTZ.js.map +7 -0
  27. package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-LE5NDSFD.js +12824 -0
  28. package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-LE5NDSFD.js.map +7 -0
  29. package/docs/.vitepress/cache/deps_temp_44e2fb0f/package.json +3 -0
  30. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vue_devtools-api.js +4505 -0
  31. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vue_devtools-api.js.map +7 -0
  32. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_core.js +583 -0
  33. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_core.js.map +7 -0
  34. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_integrations_useFocusTrap.js +1352 -0
  35. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  36. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___mark__js_src_vanilla__js.js +1665 -0
  37. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  38. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___minisearch.js +1813 -0
  39. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___minisearch.js.map +7 -0
  40. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vue.js +347 -0
  41. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vue.js.map +7 -0
  42. package/docs/.vitepress/config.ts +167 -0
  43. package/docs/.vitepress/theme/custom.css +39 -0
  44. package/docs/.vitepress/theme/index.ts +4 -0
  45. package/docs/README.md +82 -0
  46. package/docs/commands/branch.md +468 -0
  47. package/docs/commands/commit.md +554 -0
  48. package/docs/commands/config.md +346 -0
  49. package/docs/commands/index.md +312 -0
  50. package/docs/commands/interactive.md +384 -0
  51. package/docs/commands/release.md +300 -0
  52. package/docs/commands/stash.md +309 -0
  53. package/docs/commands/tag.md +278 -0
  54. package/docs/commands/update.md +347 -0
  55. package/docs/config/ai-config.md +160 -0
  56. package/docs/config/branch-config.md +133 -0
  57. package/docs/config/commit-config.md +185 -0
  58. package/docs/config/config-file.md +776 -0
  59. package/docs/config/examples.md +279 -0
  60. package/docs/config/index.md +478 -0
  61. package/docs/features/git-wrapped.md +199 -0
  62. package/docs/guide/ai-commit.md +576 -0
  63. package/docs/guide/basic-usage.md +522 -0
  64. package/docs/guide/best-practices.md +426 -0
  65. package/docs/guide/branch-management.md +712 -0
  66. package/docs/guide/getting-started.md +294 -0
  67. package/docs/guide/index.md +168 -0
  68. package/docs/guide/installation.md +449 -0
  69. package/docs/guide/release-management.md +744 -0
  70. package/docs/guide/stash-management.md +608 -0
  71. package/docs/guide/tag-management.md +614 -0
  72. package/docs/index.md +205 -0
  73. package/docs/public/favicon.svg +21 -0
  74. package/docs/public/hero-logo.svg +43 -0
  75. package/docs/public/logo.svg +20 -0
  76. package/package.json +12 -2
  77. package/scripts/publish.js +55 -8
  78. package/scripts/publish.sh +20 -2
  79. package/scripts/release.sh +81 -3
  80. package/scripts/update-test-count.js +55 -0
  81. package/src/ai-service.ts +107 -15
  82. package/src/commands/commit.ts +4 -0
  83. package/src/commands/init.ts +18 -0
  84. package/src/commands/log.ts +503 -0
  85. package/src/config.ts +1 -0
  86. package/src/index.ts +37 -13
  87. package/src/utils.ts +10 -0
  88. package/tests/ai-service.test.ts +237 -2
  89. package/tests/init.test.ts +582 -0
  90. package/tests/log.test.ts +106 -0
  91. package/tests/release.test.ts +333 -0
  92. package/tests/setup.ts +21 -0
  93. package/tests/stash.test.ts +376 -0
  94. package/tests/update.test.ts +402 -0
  95. package/vitest.config.ts +3 -0
  96. package/zjex-logo.svg +22 -0
  97. package/zjex-optimized.svg +34 -0
  98. package/zjex.svg +1 -0
  99. package/src/commands/help.ts +0 -76
@@ -0,0 +1,582 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { existsSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+
6
+ // Mock 所有外部依赖
7
+ vi.mock("fs", () => ({
8
+ existsSync: vi.fn(),
9
+ writeFileSync: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("path", () => ({
13
+ join: vi.fn(),
14
+ }));
15
+
16
+ vi.mock("os", () => ({
17
+ homedir: vi.fn(),
18
+ }));
19
+
20
+ vi.mock("@inquirer/prompts", () => ({
21
+ select: vi.fn(),
22
+ input: vi.fn(),
23
+ }));
24
+
25
+ vi.mock("../src/utils.js", () => ({
26
+ colors: {
27
+ bold: (text: string) => text,
28
+ green: (text: string) => text,
29
+ yellow: (text: string) => text,
30
+ cyan: (text: string) => text,
31
+ dim: (text: string) => text,
32
+ },
33
+ theme: {},
34
+ divider: vi.fn(),
35
+ }));
36
+
37
+ describe("Init 模块测试", () => {
38
+ const mockExistsSync = vi.mocked(existsSync);
39
+ const mockWriteFileSync = vi.mocked(writeFileSync);
40
+ const mockJoin = vi.mocked(join);
41
+ const mockHomedir = vi.mocked(homedir);
42
+
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ vi.spyOn(console, "log").mockImplementation(() => {});
46
+
47
+ // Default mocks
48
+ mockHomedir.mockReturnValue("/home/user");
49
+ mockJoin.mockReturnValue("/home/user/.gwrc.json");
50
+ });
51
+
52
+ afterEach(() => {
53
+ vi.restoreAllMocks();
54
+ });
55
+
56
+ describe("配置范围选择", () => {
57
+ it("应该支持全局配置", async () => {
58
+ mockExistsSync.mockReturnValue(false);
59
+
60
+ const { select, input } = await import("@inquirer/prompts");
61
+ vi.mocked(select)
62
+ .mockResolvedValueOnce("global") // 配置范围
63
+ .mockResolvedValueOnce(false) // requireId
64
+ .mockResolvedValueOnce("ask") // autoPush
65
+ .mockResolvedValueOnce(true) // autoStage
66
+ .mockResolvedValueOnce(true) // useEmoji
67
+ .mockResolvedValueOnce(false); // enableAI
68
+
69
+ vi.mocked(input)
70
+ .mockResolvedValueOnce("") // baseBranch
71
+ .mockResolvedValueOnce("feature") // featurePrefix
72
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
73
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
74
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
75
+ .mockResolvedValueOnce(""); // defaultTagPrefix
76
+
77
+ const { init } = await import("../src/commands/init.js");
78
+
79
+ await init();
80
+
81
+ expect(mockJoin).toHaveBeenCalledWith("/home/user", ".gwrc.json");
82
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
83
+ "/home/user/.gwrc.json",
84
+ expect.stringContaining('"featurePrefix": "feature"')
85
+ );
86
+ });
87
+
88
+ it("应该支持项目配置", async () => {
89
+ mockExistsSync.mockReturnValue(false);
90
+
91
+ const { select, input } = await import("@inquirer/prompts");
92
+ vi.mocked(select)
93
+ .mockResolvedValueOnce("project") // 配置范围
94
+ .mockResolvedValueOnce(false) // requireId
95
+ .mockResolvedValueOnce("ask") // autoPush
96
+ .mockResolvedValueOnce(true) // autoStage
97
+ .mockResolvedValueOnce(true) // useEmoji
98
+ .mockResolvedValueOnce(false); // enableAI
99
+
100
+ vi.mocked(input)
101
+ .mockResolvedValueOnce("") // baseBranch
102
+ .mockResolvedValueOnce("feature") // featurePrefix
103
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
104
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
105
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
106
+ .mockResolvedValueOnce(""); // defaultTagPrefix
107
+
108
+ const { init } = await import("../src/commands/init.js");
109
+
110
+ await init();
111
+
112
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
113
+ ".gwrc.json",
114
+ expect.stringContaining('"featurePrefix": "feature"')
115
+ );
116
+ });
117
+ });
118
+
119
+ describe("配置文件覆盖", () => {
120
+ it("应该处理配置文件已存在的情况", async () => {
121
+ mockExistsSync.mockReturnValue(true);
122
+
123
+ const { select, input } = await import("@inquirer/prompts");
124
+ vi.mocked(select)
125
+ .mockResolvedValueOnce("global") // 配置范围
126
+ .mockResolvedValueOnce(true) // 覆盖文件
127
+ .mockResolvedValueOnce(false) // requireId
128
+ .mockResolvedValueOnce("ask") // autoPush
129
+ .mockResolvedValueOnce(true) // autoStage
130
+ .mockResolvedValueOnce(true) // useEmoji
131
+ .mockResolvedValueOnce(false); // enableAI
132
+
133
+ vi.mocked(input)
134
+ .mockResolvedValueOnce("") // baseBranch
135
+ .mockResolvedValueOnce("feature") // featurePrefix
136
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
137
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
138
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
139
+ .mockResolvedValueOnce(""); // defaultTagPrefix
140
+
141
+ const { init } = await import("../src/commands/init.js");
142
+
143
+ await init();
144
+
145
+ expect(mockWriteFileSync).toHaveBeenCalled();
146
+ });
147
+
148
+ it("应该处理用户取消覆盖", async () => {
149
+ mockExistsSync.mockReturnValue(true);
150
+
151
+ const { select } = await import("@inquirer/prompts");
152
+ vi.mocked(select)
153
+ .mockResolvedValueOnce("global") // 配置范围
154
+ .mockResolvedValueOnce(false); // 不覆盖文件
155
+
156
+ const { init } = await import("../src/commands/init.js");
157
+
158
+ await init();
159
+
160
+ expect(console.log).toHaveBeenCalledWith("已取消");
161
+ expect(mockWriteFileSync).not.toHaveBeenCalled();
162
+ });
163
+ });
164
+
165
+ describe("基础配置", () => {
166
+ it("应该正确配置分支前缀", async () => {
167
+ mockExistsSync.mockReturnValue(false);
168
+
169
+ const { select, input } = await import("@inquirer/prompts");
170
+ vi.mocked(select)
171
+ .mockResolvedValueOnce("project") // 配置范围
172
+ .mockResolvedValueOnce(false) // requireId
173
+ .mockResolvedValueOnce("ask") // autoPush
174
+ .mockResolvedValueOnce(true) // autoStage
175
+ .mockResolvedValueOnce(true) // useEmoji
176
+ .mockResolvedValueOnce(false); // enableAI
177
+
178
+ vi.mocked(input)
179
+ .mockResolvedValueOnce("develop") // baseBranch
180
+ .mockResolvedValueOnce("feat") // featurePrefix
181
+ .mockResolvedValueOnce("fix") // hotfixPrefix
182
+ .mockResolvedValueOnce("Jira ID") // featureIdLabel
183
+ .mockResolvedValueOnce("Bug ID") // hotfixIdLabel
184
+ .mockResolvedValueOnce("v"); // defaultTagPrefix
185
+
186
+ const { init } = await import("../src/commands/init.js");
187
+
188
+ await init();
189
+
190
+ const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
191
+ const config = JSON.parse(writtenConfig);
192
+
193
+ expect(config.baseBranch).toBe("develop");
194
+ expect(config.featurePrefix).toBe("feat");
195
+ expect(config.hotfixPrefix).toBe("fix");
196
+ expect(config.featureIdLabel).toBe("Jira ID");
197
+ expect(config.hotfixIdLabel).toBe("Bug ID");
198
+ expect(config.defaultTagPrefix).toBe("v");
199
+ });
200
+
201
+ it("应该正确配置 ID 要求", async () => {
202
+ mockExistsSync.mockReturnValue(false);
203
+
204
+ const { select, input } = await import("@inquirer/prompts");
205
+ vi.mocked(select)
206
+ .mockResolvedValueOnce("project") // 配置范围
207
+ .mockResolvedValueOnce(true) // requireId
208
+ .mockResolvedValueOnce("ask") // autoPush
209
+ .mockResolvedValueOnce(true) // autoStage
210
+ .mockResolvedValueOnce(true) // useEmoji
211
+ .mockResolvedValueOnce(false); // enableAI
212
+
213
+ vi.mocked(input)
214
+ .mockResolvedValueOnce("") // baseBranch
215
+ .mockResolvedValueOnce("feature") // featurePrefix
216
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
217
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
218
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
219
+ .mockResolvedValueOnce(""); // defaultTagPrefix
220
+
221
+ const { init } = await import("../src/commands/init.js");
222
+
223
+ await init();
224
+
225
+ const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
226
+ const config = JSON.parse(writtenConfig);
227
+
228
+ expect(config.requireId).toBe(true);
229
+ });
230
+
231
+ it("应该正确配置自动推送选项", async () => {
232
+ mockExistsSync.mockReturnValue(false);
233
+
234
+ const { select, input } = await import("@inquirer/prompts");
235
+ vi.mocked(select)
236
+ .mockResolvedValueOnce("project") // 配置范围
237
+ .mockResolvedValueOnce(false) // requireId
238
+ .mockResolvedValueOnce("yes") // autoPush
239
+ .mockResolvedValueOnce(true) // autoStage
240
+ .mockResolvedValueOnce(true) // useEmoji
241
+ .mockResolvedValueOnce(false); // enableAI
242
+
243
+ vi.mocked(input)
244
+ .mockResolvedValueOnce("") // baseBranch
245
+ .mockResolvedValueOnce("feature") // featurePrefix
246
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
247
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
248
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
249
+ .mockResolvedValueOnce(""); // defaultTagPrefix
250
+
251
+ const { init } = await import("../src/commands/init.js");
252
+
253
+ await init();
254
+
255
+ const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
256
+ const config = JSON.parse(writtenConfig);
257
+
258
+ expect(config.autoPush).toBe(true);
259
+ });
260
+ });
261
+
262
+ describe("AI 配置", () => {
263
+ it("应该正确配置 GitHub Models", async () => {
264
+ mockExistsSync.mockReturnValue(false);
265
+
266
+ const { select, input } = await import("@inquirer/prompts");
267
+ vi.mocked(select)
268
+ .mockResolvedValueOnce("project") // 配置范围
269
+ .mockResolvedValueOnce(false) // requireId
270
+ .mockResolvedValueOnce("ask") // autoPush
271
+ .mockResolvedValueOnce(true) // autoStage
272
+ .mockResolvedValueOnce(true) // useEmoji
273
+ .mockResolvedValueOnce(true) // enableAI
274
+ .mockResolvedValueOnce("github") // AI provider
275
+ .mockResolvedValueOnce("zh-CN"); // language
276
+
277
+ vi.mocked(input)
278
+ .mockResolvedValueOnce("") // baseBranch
279
+ .mockResolvedValueOnce("feature") // featurePrefix
280
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
281
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
282
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
283
+ .mockResolvedValueOnce("") // defaultTagPrefix
284
+ .mockResolvedValueOnce("ghp_test_token"); // GitHub token
285
+
286
+ const { init } = await import("../src/commands/init.js");
287
+
288
+ await init();
289
+
290
+ const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
291
+ const config = JSON.parse(writtenConfig);
292
+
293
+ expect(config.aiCommit.enabled).toBe(true);
294
+ expect(config.aiCommit.provider).toBe("github");
295
+ expect(config.aiCommit.apiKey).toBe("ghp_test_token");
296
+ expect(config.aiCommit.language).toBe("zh-CN");
297
+ expect(config.aiCommit.model).toBe("gpt-4o-mini");
298
+ });
299
+
300
+ it("应该正确配置 OpenAI", async () => {
301
+ mockExistsSync.mockReturnValue(false);
302
+
303
+ const { select, input } = await import("@inquirer/prompts");
304
+ vi.mocked(select)
305
+ .mockResolvedValueOnce("project") // 配置范围
306
+ .mockResolvedValueOnce(false) // requireId
307
+ .mockResolvedValueOnce("ask") // autoPush
308
+ .mockResolvedValueOnce(true) // autoStage
309
+ .mockResolvedValueOnce(true) // useEmoji
310
+ .mockResolvedValueOnce(true) // enableAI
311
+ .mockResolvedValueOnce("openai") // AI provider
312
+ .mockResolvedValueOnce("en-US"); // language
313
+
314
+ vi.mocked(input)
315
+ .mockResolvedValueOnce("") // baseBranch
316
+ .mockResolvedValueOnce("feature") // featurePrefix
317
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
318
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
319
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
320
+ .mockResolvedValueOnce("") // defaultTagPrefix
321
+ .mockResolvedValueOnce("sk-test-key"); // OpenAI API key
322
+
323
+ const { init } = await import("../src/commands/init.js");
324
+
325
+ await init();
326
+
327
+ const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
328
+ const config = JSON.parse(writtenConfig);
329
+
330
+ expect(config.aiCommit.enabled).toBe(true);
331
+ expect(config.aiCommit.provider).toBe("openai");
332
+ expect(config.aiCommit.apiKey).toBe("sk-test-key");
333
+ expect(config.aiCommit.language).toBe("en-US");
334
+ expect(config.aiCommit.model).toBe("gpt-4o-mini");
335
+ });
336
+
337
+ it("应该正确配置 Ollama", async () => {
338
+ mockExistsSync.mockReturnValue(false);
339
+
340
+ const { select, input } = await import("@inquirer/prompts");
341
+ vi.mocked(select)
342
+ .mockResolvedValueOnce("project") // 配置范围
343
+ .mockResolvedValueOnce(false) // requireId
344
+ .mockResolvedValueOnce("ask") // autoPush
345
+ .mockResolvedValueOnce(true) // autoStage
346
+ .mockResolvedValueOnce(true) // useEmoji
347
+ .mockResolvedValueOnce(true) // enableAI
348
+ .mockResolvedValueOnce("ollama") // AI provider
349
+ .mockResolvedValueOnce("zh-CN"); // language
350
+
351
+ vi.mocked(input)
352
+ .mockResolvedValueOnce("") // baseBranch
353
+ .mockResolvedValueOnce("feature") // featurePrefix
354
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
355
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
356
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
357
+ .mockResolvedValueOnce(""); // defaultTagPrefix
358
+
359
+ const { init } = await import("../src/commands/init.js");
360
+
361
+ await init();
362
+
363
+ const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
364
+ const config = JSON.parse(writtenConfig);
365
+
366
+ expect(config.aiCommit.enabled).toBe(true);
367
+ expect(config.aiCommit.provider).toBe("ollama");
368
+ expect(config.aiCommit.apiKey).toBeUndefined();
369
+ expect(config.aiCommit.language).toBe("zh-CN");
370
+ expect(config.aiCommit.model).toBe("qwen2.5-coder:7b");
371
+ });
372
+
373
+ it("应该正确配置禁用 AI", async () => {
374
+ mockExistsSync.mockReturnValue(false);
375
+
376
+ const { select, input } = await import("@inquirer/prompts");
377
+ vi.mocked(select)
378
+ .mockResolvedValueOnce("project") // 配置范围
379
+ .mockResolvedValueOnce(false) // requireId
380
+ .mockResolvedValueOnce("ask") // autoPush
381
+ .mockResolvedValueOnce(true) // autoStage
382
+ .mockResolvedValueOnce(true) // useEmoji
383
+ .mockResolvedValueOnce(false); // enableAI
384
+
385
+ vi.mocked(input)
386
+ .mockResolvedValueOnce("") // baseBranch
387
+ .mockResolvedValueOnce("feature") // featurePrefix
388
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
389
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
390
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
391
+ .mockResolvedValueOnce(""); // defaultTagPrefix
392
+
393
+ const { init } = await import("../src/commands/init.js");
394
+
395
+ await init();
396
+
397
+ const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
398
+ const config = JSON.parse(writtenConfig);
399
+
400
+ expect(config.aiCommit.enabled).toBe(false);
401
+ });
402
+ });
403
+
404
+ describe("配置验证", () => {
405
+ it("应该验证 GitHub Token 不为空", async () => {
406
+ mockExistsSync.mockReturnValue(false);
407
+
408
+ const { select, input } = await import("@inquirer/prompts");
409
+ vi.mocked(select)
410
+ .mockResolvedValueOnce("project") // 配置范围
411
+ .mockResolvedValueOnce(false) // requireId
412
+ .mockResolvedValueOnce("ask") // autoPush
413
+ .mockResolvedValueOnce(true) // autoStage
414
+ .mockResolvedValueOnce(true) // useEmoji
415
+ .mockResolvedValueOnce(true) // enableAI
416
+ .mockResolvedValueOnce("github") // AI provider
417
+ .mockResolvedValueOnce("zh-CN"); // language
418
+
419
+ vi.mocked(input)
420
+ .mockResolvedValueOnce("") // baseBranch
421
+ .mockResolvedValueOnce("feature") // featurePrefix
422
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
423
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
424
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
425
+ .mockResolvedValueOnce("") // defaultTagPrefix
426
+ .mockResolvedValueOnce("ghp_valid_token"); // GitHub token
427
+
428
+ const { init } = await import("../src/commands/init.js");
429
+
430
+ await init();
431
+
432
+ // 验证 input 被调用时包含验证函数
433
+ const inputCalls = vi.mocked(input).mock.calls;
434
+ const tokenInputCall = inputCalls.find(call =>
435
+ call[0].message?.includes("GitHub Token")
436
+ );
437
+
438
+ expect(tokenInputCall).toBeDefined();
439
+ expect(tokenInputCall![0].validate).toBeDefined();
440
+
441
+ // 测试验证函数
442
+ const validate = tokenInputCall![0].validate!;
443
+ expect(validate("")).toBe("GitHub Token 不能为空");
444
+ expect(validate("valid-token")).toBe(true);
445
+ });
446
+
447
+ it("应该验证 OpenAI API Key 不为空", async () => {
448
+ mockExistsSync.mockReturnValue(false);
449
+
450
+ const { select, input } = await import("@inquirer/prompts");
451
+ vi.mocked(select)
452
+ .mockResolvedValueOnce("project") // 配置范围
453
+ .mockResolvedValueOnce(false) // requireId
454
+ .mockResolvedValueOnce("ask") // autoPush
455
+ .mockResolvedValueOnce(true) // autoStage
456
+ .mockResolvedValueOnce(true) // useEmoji
457
+ .mockResolvedValueOnce(true) // enableAI
458
+ .mockResolvedValueOnce("openai") // AI provider
459
+ .mockResolvedValueOnce("en-US"); // language
460
+
461
+ vi.mocked(input)
462
+ .mockResolvedValueOnce("") // baseBranch
463
+ .mockResolvedValueOnce("feature") // featurePrefix
464
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
465
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
466
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
467
+ .mockResolvedValueOnce("") // defaultTagPrefix
468
+ .mockResolvedValueOnce("sk-valid-key"); // OpenAI API key
469
+
470
+ const { init } = await import("../src/commands/init.js");
471
+
472
+ await init();
473
+
474
+ // 验证 input 被调用时包含验证函数
475
+ const inputCalls = vi.mocked(input).mock.calls;
476
+ const keyInputCall = inputCalls.find(call =>
477
+ call[0].message?.includes("OpenAI API Key")
478
+ );
479
+
480
+ expect(keyInputCall).toBeDefined();
481
+ expect(keyInputCall![0].validate).toBeDefined();
482
+
483
+ // 测试验证函数
484
+ const validate = keyInputCall![0].validate!;
485
+ expect(validate("")).toBe("API Key 不能为空");
486
+ expect(validate("valid-key")).toBe(true);
487
+ });
488
+ });
489
+
490
+ describe("配置输出", () => {
491
+ it("应该包含默认的 commit emojis", async () => {
492
+ mockExistsSync.mockReturnValue(false);
493
+
494
+ const { select, input } = await import("@inquirer/prompts");
495
+ vi.mocked(select)
496
+ .mockResolvedValueOnce("project") // 配置范围
497
+ .mockResolvedValueOnce(false) // requireId
498
+ .mockResolvedValueOnce("ask") // autoPush
499
+ .mockResolvedValueOnce(true) // autoStage
500
+ .mockResolvedValueOnce(true) // useEmoji
501
+ .mockResolvedValueOnce(false); // enableAI
502
+
503
+ vi.mocked(input)
504
+ .mockResolvedValueOnce("") // baseBranch
505
+ .mockResolvedValueOnce("feature") // featurePrefix
506
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
507
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
508
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
509
+ .mockResolvedValueOnce(""); // defaultTagPrefix
510
+
511
+ const { init } = await import("../src/commands/init.js");
512
+
513
+ await init();
514
+
515
+ const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
516
+ const config = JSON.parse(writtenConfig);
517
+
518
+ expect(config.commitEmojis).toBeDefined();
519
+ expect(config.commitEmojis.feat).toBe("✨");
520
+ expect(config.commitEmojis.fix).toBe("🐛");
521
+ expect(config.commitEmojis.docs).toBe("📝");
522
+ });
523
+
524
+ it("应该显示成功消息", async () => {
525
+ mockExistsSync.mockReturnValue(false);
526
+
527
+ const { select, input } = await import("@inquirer/prompts");
528
+ vi.mocked(select)
529
+ .mockResolvedValueOnce("global") // 配置范围
530
+ .mockResolvedValueOnce(false) // requireId
531
+ .mockResolvedValueOnce("ask") // autoPush
532
+ .mockResolvedValueOnce(true) // autoStage
533
+ .mockResolvedValueOnce(true) // useEmoji
534
+ .mockResolvedValueOnce(false); // enableAI
535
+
536
+ vi.mocked(input)
537
+ .mockResolvedValueOnce("") // baseBranch
538
+ .mockResolvedValueOnce("feature") // featurePrefix
539
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
540
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
541
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
542
+ .mockResolvedValueOnce(""); // defaultTagPrefix
543
+
544
+ const { init } = await import("../src/commands/init.js");
545
+
546
+ await init();
547
+
548
+ expect(console.log).toHaveBeenCalledWith(
549
+ expect.stringContaining("✓ 配置已保存到 全局配置文件")
550
+ );
551
+ });
552
+
553
+ it("应该显示全局配置的提示信息", async () => {
554
+ mockExistsSync.mockReturnValue(false);
555
+
556
+ const { select, input } = await import("@inquirer/prompts");
557
+ vi.mocked(select)
558
+ .mockResolvedValueOnce("global") // 配置范围
559
+ .mockResolvedValueOnce(false) // requireId
560
+ .mockResolvedValueOnce("ask") // autoPush
561
+ .mockResolvedValueOnce(true) // autoStage
562
+ .mockResolvedValueOnce(true) // useEmoji
563
+ .mockResolvedValueOnce(false); // enableAI
564
+
565
+ vi.mocked(input)
566
+ .mockResolvedValueOnce("") // baseBranch
567
+ .mockResolvedValueOnce("feature") // featurePrefix
568
+ .mockResolvedValueOnce("hotfix") // hotfixPrefix
569
+ .mockResolvedValueOnce("Story ID") // featureIdLabel
570
+ .mockResolvedValueOnce("Issue ID") // hotfixIdLabel
571
+ .mockResolvedValueOnce(""); // defaultTagPrefix
572
+
573
+ const { init } = await import("../src/commands/init.js");
574
+
575
+ await init();
576
+
577
+ expect(console.log).toHaveBeenCalledWith(
578
+ expect.stringContaining("全局配置对所有项目生效")
579
+ );
580
+ });
581
+ });
582
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @zjex/git-workflow - Log 命令测试
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { execSync } from 'child_process';
7
+
8
+ // Mock execSync
9
+ vi.mock('child_process', () => ({
10
+ execSync: vi.fn()
11
+ }));
12
+
13
+ // Mock inquirer prompts
14
+ vi.mock('@inquirer/prompts', () => ({
15
+ select: vi.fn(),
16
+ input: vi.fn(),
17
+ confirm: vi.fn()
18
+ }));
19
+
20
+ const mockExecSync = vi.mocked(execSync);
21
+
22
+ describe('Log 命令', () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ describe('buildLogCommand', () => {
28
+ it('应该构建基本的log命令', async () => {
29
+ // 动态导入以避免模块加载时的副作用
30
+ const { log } = await import('../src/commands/log.js');
31
+
32
+ // Mock execSync 返回值
33
+ mockExecSync.mockReturnValue('mock log output');
34
+
35
+ // 测试基本选项
36
+ const options = { format: 'medium' as const, limit: 10 };
37
+
38
+ // 由于log函数会调用execSync,我们需要模拟它
39
+ await expect(async () => {
40
+ // 这里我们不能直接测试buildLogCommand,因为它不是导出的
41
+ // 但我们可以通过测试log函数来间接测试
42
+ }).not.toThrow();
43
+ });
44
+
45
+ it('应该正确处理作者筛选', () => {
46
+ // 测试作者筛选参数
47
+ expect(mockExecSync).toBeDefined();
48
+ });
49
+
50
+ it('应该正确处理时间范围', () => {
51
+ // 测试时间范围参数
52
+ expect(mockExecSync).toBeDefined();
53
+ });
54
+
55
+ it('应该正确处理关键词搜索', () => {
56
+ // 测试关键词搜索参数
57
+ expect(mockExecSync).toBeDefined();
58
+ });
59
+ });
60
+
61
+ describe('日志格式', () => {
62
+ it('应该支持oneline格式', () => {
63
+ expect(true).toBe(true); // 占位测试
64
+ });
65
+
66
+ it('应该支持graph格式', () => {
67
+ expect(true).toBe(true); // 占位测试
68
+ });
69
+
70
+ it('应该支持medium格式', () => {
71
+ expect(true).toBe(true); // 占位测试
72
+ });
73
+ });
74
+
75
+ describe('错误处理', () => {
76
+ it('应该处理Git命令执行失败', async () => {
77
+ mockExecSync.mockImplementation(() => {
78
+ const error = new Error('Command failed') as any;
79
+ error.status = 128;
80
+ throw error;
81
+ });
82
+
83
+ // 测试错误处理
84
+ expect(mockExecSync).toBeDefined();
85
+ });
86
+
87
+ it('应该处理空的日志输出', () => {
88
+ mockExecSync.mockReturnValue('');
89
+ expect(mockExecSync).toBeDefined();
90
+ });
91
+ });
92
+
93
+ describe('快速日志功能', () => {
94
+ it('应该支持快速查看最近提交', async () => {
95
+ mockExecSync.mockReturnValue('abc123 Test commit');
96
+
97
+ const { quickLog } = await import('../src/commands/log.js');
98
+
99
+ await expect(quickLog(5)).resolves.not.toThrow();
100
+ expect(mockExecSync).toHaveBeenCalledWith(
101
+ expect.stringContaining('-5'),
102
+ expect.any(Object)
103
+ );
104
+ });
105
+ });
106
+ });