@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,705 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
generateAICommitMessage,
|
|
4
|
+
isAICommitAvailable,
|
|
5
|
+
getProviderInfo,
|
|
6
|
+
} from "../src/ai-service";
|
|
7
|
+
import { execOutput } from "../src/utils";
|
|
8
|
+
import type { GwConfig } from "../src/config";
|
|
9
|
+
|
|
10
|
+
// Mock dependencies
|
|
11
|
+
vi.mock("../src/utils");
|
|
12
|
+
global.fetch = vi.fn();
|
|
13
|
+
|
|
14
|
+
describe("AI Service 模块测试", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("isAICommitAvailable 函数", () => {
|
|
24
|
+
it("默认应该返回 true", () => {
|
|
25
|
+
const config: GwConfig = {
|
|
26
|
+
featurePrefix: "feature",
|
|
27
|
+
hotfixPrefix: "hotfix",
|
|
28
|
+
requireId: false,
|
|
29
|
+
featureIdLabel: "Story ID",
|
|
30
|
+
hotfixIdLabel: "Issue ID",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
expect(isAICommitAvailable(config)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("明确启用时应该返回 true", () => {
|
|
37
|
+
const config: GwConfig = {
|
|
38
|
+
featurePrefix: "feature",
|
|
39
|
+
hotfixPrefix: "hotfix",
|
|
40
|
+
requireId: false,
|
|
41
|
+
featureIdLabel: "Story ID",
|
|
42
|
+
hotfixIdLabel: "Issue ID",
|
|
43
|
+
aiCommit: {
|
|
44
|
+
enabled: true,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(isAICommitAvailable(config)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("明确禁用时应该返回 false", () => {
|
|
52
|
+
const config: GwConfig = {
|
|
53
|
+
featurePrefix: "feature",
|
|
54
|
+
hotfixPrefix: "hotfix",
|
|
55
|
+
requireId: false,
|
|
56
|
+
featureIdLabel: "Story ID",
|
|
57
|
+
hotfixIdLabel: "Issue ID",
|
|
58
|
+
aiCommit: {
|
|
59
|
+
enabled: false,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
expect(isAICommitAvailable(config)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("getProviderInfo 函数", () => {
|
|
68
|
+
it("应该返回 GitHub 提供商信息", () => {
|
|
69
|
+
const info = getProviderInfo("github");
|
|
70
|
+
|
|
71
|
+
expect(info).toBeDefined();
|
|
72
|
+
expect(info?.name).toBe("GitHub Models");
|
|
73
|
+
expect(info?.free).toBe(true);
|
|
74
|
+
expect(info?.needsKey).toBe(true);
|
|
75
|
+
expect(info?.defaultModel).toBe("gpt-4o-mini");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("应该返回 OpenAI 提供商信息", () => {
|
|
79
|
+
const info = getProviderInfo("openai");
|
|
80
|
+
|
|
81
|
+
expect(info).toBeDefined();
|
|
82
|
+
expect(info?.name).toBe("OpenAI");
|
|
83
|
+
expect(info?.free).toBe(false);
|
|
84
|
+
expect(info?.needsKey).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("应该返回 Claude 提供商信息", () => {
|
|
88
|
+
const info = getProviderInfo("claude");
|
|
89
|
+
|
|
90
|
+
expect(info).toBeDefined();
|
|
91
|
+
expect(info?.name).toBe("Claude");
|
|
92
|
+
expect(info?.free).toBe(false);
|
|
93
|
+
expect(info?.needsKey).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("应该返回 Ollama 提供商信息", () => {
|
|
97
|
+
const info = getProviderInfo("ollama");
|
|
98
|
+
|
|
99
|
+
expect(info).toBeDefined();
|
|
100
|
+
expect(info?.name).toBe("Ollama");
|
|
101
|
+
expect(info?.free).toBe(true);
|
|
102
|
+
expect(info?.needsKey).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("不支持的提供商应该返回 null", () => {
|
|
106
|
+
const info = getProviderInfo("unknown");
|
|
107
|
+
|
|
108
|
+
expect(info).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("generateAICommitMessage 函数", () => {
|
|
113
|
+
const mockConfig: GwConfig = {
|
|
114
|
+
featurePrefix: "feature",
|
|
115
|
+
hotfixPrefix: "hotfix",
|
|
116
|
+
requireId: false,
|
|
117
|
+
featureIdLabel: "Story ID",
|
|
118
|
+
hotfixIdLabel: "Issue ID",
|
|
119
|
+
aiCommit: {
|
|
120
|
+
enabled: true,
|
|
121
|
+
provider: "github",
|
|
122
|
+
apiKey: "test-key",
|
|
123
|
+
language: "zh-CN",
|
|
124
|
+
// 不设置 maxTokens,让它自动计算
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
it("没有代码更改时应该抛出错误", async () => {
|
|
129
|
+
vi.mocked(execOutput).mockReturnValue("");
|
|
130
|
+
|
|
131
|
+
await expect(generateAICommitMessage(mockConfig)).rejects.toThrow(
|
|
132
|
+
"没有检测到代码更改"
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("应该调用 GitHub API", async () => {
|
|
137
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
138
|
+
|
|
139
|
+
const mockFetch = vi.mocked(fetch);
|
|
140
|
+
mockFetch.mockResolvedValue({
|
|
141
|
+
ok: true,
|
|
142
|
+
json: async () => ({
|
|
143
|
+
choices: [
|
|
144
|
+
{
|
|
145
|
+
message: {
|
|
146
|
+
content: "feat(test): 添加测试功能",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
} as Response);
|
|
152
|
+
|
|
153
|
+
const result = await generateAICommitMessage(mockConfig);
|
|
154
|
+
|
|
155
|
+
expect(result).toBe("feat(test): 添加测试功能");
|
|
156
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
157
|
+
expect.stringContaining("github.ai"),
|
|
158
|
+
expect.objectContaining({
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: expect.objectContaining({
|
|
161
|
+
Authorization: "Bearer test-key",
|
|
162
|
+
}),
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("应该调用 OpenAI API", async () => {
|
|
168
|
+
const openaiConfig = {
|
|
169
|
+
...mockConfig,
|
|
170
|
+
aiCommit: {
|
|
171
|
+
...mockConfig.aiCommit!,
|
|
172
|
+
provider: "openai" as const,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
177
|
+
|
|
178
|
+
const mockFetch = vi.mocked(fetch);
|
|
179
|
+
mockFetch.mockResolvedValue({
|
|
180
|
+
ok: true,
|
|
181
|
+
json: async () => ({
|
|
182
|
+
choices: [
|
|
183
|
+
{
|
|
184
|
+
message: {
|
|
185
|
+
content: "feat(test): add test feature",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
}),
|
|
190
|
+
} as Response);
|
|
191
|
+
|
|
192
|
+
const result = await generateAICommitMessage(openaiConfig);
|
|
193
|
+
|
|
194
|
+
expect(result).toBe("feat(test): add test feature");
|
|
195
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
196
|
+
expect.stringContaining("openai.com"),
|
|
197
|
+
expect.any(Object)
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("应该调用 Claude API", async () => {
|
|
202
|
+
const claudeConfig = {
|
|
203
|
+
...mockConfig,
|
|
204
|
+
aiCommit: {
|
|
205
|
+
...mockConfig.aiCommit!,
|
|
206
|
+
provider: "claude" as const,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
211
|
+
|
|
212
|
+
const mockFetch = vi.mocked(fetch);
|
|
213
|
+
mockFetch.mockResolvedValue({
|
|
214
|
+
ok: true,
|
|
215
|
+
json: async () => ({
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
text: "feat(test): add test feature",
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
}),
|
|
222
|
+
} as Response);
|
|
223
|
+
|
|
224
|
+
const result = await generateAICommitMessage(claudeConfig);
|
|
225
|
+
|
|
226
|
+
expect(result).toBe("feat(test): add test feature");
|
|
227
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
228
|
+
expect.stringContaining("anthropic.com"),
|
|
229
|
+
expect.objectContaining({
|
|
230
|
+
headers: expect.objectContaining({
|
|
231
|
+
"x-api-key": "test-key",
|
|
232
|
+
}),
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("应该调用 Ollama API", async () => {
|
|
238
|
+
const ollamaConfig = {
|
|
239
|
+
...mockConfig,
|
|
240
|
+
aiCommit: {
|
|
241
|
+
...mockConfig.aiCommit!,
|
|
242
|
+
provider: "ollama" as const,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
247
|
+
|
|
248
|
+
const mockFetch = vi.mocked(fetch);
|
|
249
|
+
mockFetch.mockResolvedValue({
|
|
250
|
+
ok: true,
|
|
251
|
+
json: async () => ({
|
|
252
|
+
response: "feat(test): 添加测试功能",
|
|
253
|
+
}),
|
|
254
|
+
} as Response);
|
|
255
|
+
|
|
256
|
+
const result = await generateAICommitMessage(ollamaConfig);
|
|
257
|
+
|
|
258
|
+
expect(result).toBe("feat(test): 添加测试功能");
|
|
259
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
260
|
+
expect.stringContaining("localhost:11434"),
|
|
261
|
+
expect.any(Object)
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("API 失败时应该抛出错误", async () => {
|
|
266
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
267
|
+
|
|
268
|
+
const mockFetch = vi.mocked(fetch);
|
|
269
|
+
mockFetch.mockResolvedValue({
|
|
270
|
+
ok: false,
|
|
271
|
+
status: 401,
|
|
272
|
+
text: async () => "Unauthorized",
|
|
273
|
+
} as Response);
|
|
274
|
+
|
|
275
|
+
await expect(generateAICommitMessage(mockConfig)).rejects.toThrow(
|
|
276
|
+
"GitHub Models API 错误"
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("缺少 API key 时应该抛出错误", async () => {
|
|
281
|
+
const configWithoutKey = {
|
|
282
|
+
...mockConfig,
|
|
283
|
+
aiCommit: {
|
|
284
|
+
...mockConfig.aiCommit!,
|
|
285
|
+
apiKey: "",
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
290
|
+
|
|
291
|
+
await expect(generateAICommitMessage(configWithoutKey)).rejects.toThrow(
|
|
292
|
+
"需要 API key"
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("不支持的提供商应该抛出错误", async () => {
|
|
297
|
+
const invalidConfig = {
|
|
298
|
+
...mockConfig,
|
|
299
|
+
aiCommit: {
|
|
300
|
+
...mockConfig.aiCommit!,
|
|
301
|
+
provider: "invalid" as any,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
306
|
+
|
|
307
|
+
await expect(generateAICommitMessage(invalidConfig)).rejects.toThrow(
|
|
308
|
+
"不支持的 AI 提供商"
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("应该截断过长的 diff", async () => {
|
|
313
|
+
const longDiff = "a".repeat(7000); // 超过详细模式的 6000 限制
|
|
314
|
+
vi.mocked(execOutput).mockReturnValue(longDiff);
|
|
315
|
+
|
|
316
|
+
const mockFetch = vi.mocked(fetch);
|
|
317
|
+
mockFetch.mockResolvedValue({
|
|
318
|
+
ok: true,
|
|
319
|
+
json: async () => ({
|
|
320
|
+
choices: [
|
|
321
|
+
{
|
|
322
|
+
message: {
|
|
323
|
+
content: "feat: update",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
}),
|
|
328
|
+
} as Response);
|
|
329
|
+
|
|
330
|
+
await generateAICommitMessage(mockConfig);
|
|
331
|
+
|
|
332
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
333
|
+
const body = JSON.parse(callArgs.body as string);
|
|
334
|
+
const prompt = body.messages[0].content;
|
|
335
|
+
|
|
336
|
+
expect(prompt.length).toBeLessThan(longDiff.length + 1000);
|
|
337
|
+
expect(prompt).toContain("...");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("应该使用配置的语言", async () => {
|
|
341
|
+
const enConfig = {
|
|
342
|
+
...mockConfig,
|
|
343
|
+
aiCommit: {
|
|
344
|
+
...mockConfig.aiCommit!,
|
|
345
|
+
language: "en-US" as const,
|
|
346
|
+
detailedDescription: false, // 使用简洁模式测试语言
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
351
|
+
|
|
352
|
+
const mockFetch = vi.mocked(fetch);
|
|
353
|
+
mockFetch.mockResolvedValue({
|
|
354
|
+
ok: true,
|
|
355
|
+
json: async () => ({
|
|
356
|
+
choices: [
|
|
357
|
+
{
|
|
358
|
+
message: {
|
|
359
|
+
content: "feat: add feature",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
}),
|
|
364
|
+
} as Response);
|
|
365
|
+
|
|
366
|
+
await generateAICommitMessage(enConfig);
|
|
367
|
+
|
|
368
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
369
|
+
const body = JSON.parse(callArgs.body as string);
|
|
370
|
+
const prompt = body.messages[0].content;
|
|
371
|
+
|
|
372
|
+
expect(prompt).toContain("Generate a commit message");
|
|
373
|
+
expect(prompt).not.toContain("生成");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("应该使用配置的模型", async () => {
|
|
377
|
+
const customModelConfig = {
|
|
378
|
+
...mockConfig,
|
|
379
|
+
aiCommit: {
|
|
380
|
+
...mockConfig.aiCommit!,
|
|
381
|
+
model: "gpt-4",
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
386
|
+
|
|
387
|
+
const mockFetch = vi.mocked(fetch);
|
|
388
|
+
mockFetch.mockResolvedValue({
|
|
389
|
+
ok: true,
|
|
390
|
+
json: async () => ({
|
|
391
|
+
choices: [
|
|
392
|
+
{
|
|
393
|
+
message: {
|
|
394
|
+
content: "feat: add feature",
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
}),
|
|
399
|
+
} as Response);
|
|
400
|
+
|
|
401
|
+
await generateAICommitMessage(customModelConfig);
|
|
402
|
+
|
|
403
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
404
|
+
const body = JSON.parse(callArgs.body as string);
|
|
405
|
+
|
|
406
|
+
expect(body.model).toBe("gpt-4");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("应该使用配置的 maxTokens", async () => {
|
|
410
|
+
const customTokensConfig = {
|
|
411
|
+
...mockConfig,
|
|
412
|
+
aiCommit: {
|
|
413
|
+
...mockConfig.aiCommit!,
|
|
414
|
+
maxTokens: 500,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
419
|
+
|
|
420
|
+
const mockFetch = vi.mocked(fetch);
|
|
421
|
+
mockFetch.mockResolvedValue({
|
|
422
|
+
ok: true,
|
|
423
|
+
json: async () => ({
|
|
424
|
+
choices: [
|
|
425
|
+
{
|
|
426
|
+
message: {
|
|
427
|
+
content: "feat: add feature",
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
}),
|
|
432
|
+
} as Response);
|
|
433
|
+
|
|
434
|
+
await generateAICommitMessage(customTokensConfig);
|
|
435
|
+
|
|
436
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
437
|
+
const body = JSON.parse(callArgs.body as string);
|
|
438
|
+
|
|
439
|
+
expect(body.max_tokens).toBe(500);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("默认应该启用详细描述模式", async () => {
|
|
443
|
+
const defaultConfig = {
|
|
444
|
+
...mockConfig,
|
|
445
|
+
aiCommit: {
|
|
446
|
+
...mockConfig.aiCommit!,
|
|
447
|
+
// 不设置 detailedDescription 和 maxTokens,测试默认行为
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
// 移除 maxTokens 设置
|
|
451
|
+
delete defaultConfig.aiCommit!.maxTokens;
|
|
452
|
+
|
|
453
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
454
|
+
|
|
455
|
+
const mockFetch = vi.mocked(fetch);
|
|
456
|
+
mockFetch.mockResolvedValue({
|
|
457
|
+
ok: true,
|
|
458
|
+
json: async () => ({
|
|
459
|
+
choices: [
|
|
460
|
+
{
|
|
461
|
+
message: {
|
|
462
|
+
content: "feat(auth): 添加用户登录功能\n\n- 实现用户名密码登录接口\n- 添加登录状态验证中间件",
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
}),
|
|
467
|
+
} as Response);
|
|
468
|
+
|
|
469
|
+
const result = await generateAICommitMessage(defaultConfig);
|
|
470
|
+
|
|
471
|
+
expect(result).toContain("feat(auth): 添加用户登录功能");
|
|
472
|
+
expect(result).toContain("- 实现用户名密码登录接口");
|
|
473
|
+
|
|
474
|
+
// 验证使用了详细描述的 prompt
|
|
475
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
476
|
+
const body = JSON.parse(callArgs.body as string);
|
|
477
|
+
const prompt = body.messages[0].content;
|
|
478
|
+
|
|
479
|
+
expect(prompt).toContain("详细描述:列出主要修改点");
|
|
480
|
+
expect(body.max_tokens).toBe(400); // 详细模式的 token 数
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("详细描述模式应该生成包含修改点的 commit message", async () => {
|
|
484
|
+
const detailedConfig = {
|
|
485
|
+
...mockConfig,
|
|
486
|
+
aiCommit: {
|
|
487
|
+
...mockConfig.aiCommit!,
|
|
488
|
+
detailedDescription: true,
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
493
|
+
|
|
494
|
+
const mockFetch = vi.mocked(fetch);
|
|
495
|
+
mockFetch.mockResolvedValue({
|
|
496
|
+
ok: true,
|
|
497
|
+
json: async () => ({
|
|
498
|
+
choices: [
|
|
499
|
+
{
|
|
500
|
+
message: {
|
|
501
|
+
content: "feat(auth): 添加用户登录功能\n\n- 实现用户名密码登录接口\n- 添加登录状态验证中间件\n- 完善登录错误处理逻辑",
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
}),
|
|
506
|
+
} as Response);
|
|
507
|
+
|
|
508
|
+
const result = await generateAICommitMessage(detailedConfig);
|
|
509
|
+
|
|
510
|
+
expect(result).toContain("feat(auth): 添加用户登录功能");
|
|
511
|
+
expect(result).toContain("- 实现用户名密码登录接口");
|
|
512
|
+
expect(result).toContain("- 添加登录状态验证中间件");
|
|
513
|
+
expect(result).toContain("- 完善登录错误处理逻辑");
|
|
514
|
+
|
|
515
|
+
// 验证 prompt 包含详细描述的指令
|
|
516
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
517
|
+
const body = JSON.parse(callArgs.body as string);
|
|
518
|
+
const prompt = body.messages[0].content;
|
|
519
|
+
|
|
520
|
+
expect(prompt).toContain("详细描述:列出主要修改点");
|
|
521
|
+
expect(prompt).toContain("以 \"- \" 开头");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("详细描述模式应该使用更大的 maxTokens", async () => {
|
|
525
|
+
const detailedConfig = {
|
|
526
|
+
...mockConfig,
|
|
527
|
+
aiCommit: {
|
|
528
|
+
...mockConfig.aiCommit!,
|
|
529
|
+
// 不设置 detailedDescription 和 maxTokens,使用默认值
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
// 移除 maxTokens 设置
|
|
533
|
+
delete detailedConfig.aiCommit!.maxTokens;
|
|
534
|
+
|
|
535
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
536
|
+
|
|
537
|
+
const mockFetch = vi.mocked(fetch);
|
|
538
|
+
mockFetch.mockResolvedValue({
|
|
539
|
+
ok: true,
|
|
540
|
+
json: async () => ({
|
|
541
|
+
choices: [
|
|
542
|
+
{
|
|
543
|
+
message: {
|
|
544
|
+
content: "feat: add feature\n\n- change 1\n- change 2",
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
}),
|
|
549
|
+
} as Response);
|
|
550
|
+
|
|
551
|
+
await generateAICommitMessage(detailedConfig);
|
|
552
|
+
|
|
553
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
554
|
+
const body = JSON.parse(callArgs.body as string);
|
|
555
|
+
|
|
556
|
+
// 默认启用详细描述模式,应该使用 400 tokens
|
|
557
|
+
expect(body.max_tokens).toBe(400);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("详细描述模式应该允许更长的 diff", async () => {
|
|
561
|
+
const detailedConfig = {
|
|
562
|
+
...mockConfig,
|
|
563
|
+
aiCommit: {
|
|
564
|
+
...mockConfig.aiCommit!,
|
|
565
|
+
// 不设置 detailedDescription,使用默认值 true
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const longDiff = "a".repeat(5500); // 超过简洁模式的 4000 限制,但在详细模式的 6000 限制内
|
|
570
|
+
vi.mocked(execOutput).mockReturnValue(longDiff);
|
|
571
|
+
|
|
572
|
+
const mockFetch = vi.mocked(fetch);
|
|
573
|
+
mockFetch.mockResolvedValue({
|
|
574
|
+
ok: true,
|
|
575
|
+
json: async () => ({
|
|
576
|
+
choices: [
|
|
577
|
+
{
|
|
578
|
+
message: {
|
|
579
|
+
content: "feat: update",
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
}),
|
|
584
|
+
} as Response);
|
|
585
|
+
|
|
586
|
+
await generateAICommitMessage(detailedConfig);
|
|
587
|
+
|
|
588
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
589
|
+
const body = JSON.parse(callArgs.body as string);
|
|
590
|
+
const prompt = body.messages[0].content;
|
|
591
|
+
|
|
592
|
+
// 在详细模式下,5500 字符的 diff 不应该被截断
|
|
593
|
+
expect(prompt).toContain(longDiff);
|
|
594
|
+
expect(prompt).not.toContain("...");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("简洁模式应该生成简短的 commit message", async () => {
|
|
598
|
+
const simpleConfig = {
|
|
599
|
+
...mockConfig,
|
|
600
|
+
aiCommit: {
|
|
601
|
+
...mockConfig.aiCommit!,
|
|
602
|
+
detailedDescription: false,
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
607
|
+
|
|
608
|
+
const mockFetch = vi.mocked(fetch);
|
|
609
|
+
mockFetch.mockResolvedValue({
|
|
610
|
+
ok: true,
|
|
611
|
+
json: async () => ({
|
|
612
|
+
choices: [
|
|
613
|
+
{
|
|
614
|
+
message: {
|
|
615
|
+
content: "feat(auth): 添加用户登录功能",
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
}),
|
|
620
|
+
} as Response);
|
|
621
|
+
|
|
622
|
+
const result = await generateAICommitMessage(simpleConfig);
|
|
623
|
+
|
|
624
|
+
expect(result).toBe("feat(auth): 添加用户登录功能");
|
|
625
|
+
|
|
626
|
+
// 验证 prompt 不包含详细描述的指令
|
|
627
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
628
|
+
const body = JSON.parse(callArgs.body as string);
|
|
629
|
+
const prompt = body.messages[0].content;
|
|
630
|
+
|
|
631
|
+
expect(prompt).not.toContain("详细描述:列出主要修改点");
|
|
632
|
+
expect(prompt).toContain("只返回一条 commit message");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("英文详细描述模式应该使用英文指令", async () => {
|
|
636
|
+
const enDetailedConfig = {
|
|
637
|
+
...mockConfig,
|
|
638
|
+
aiCommit: {
|
|
639
|
+
...mockConfig.aiCommit!,
|
|
640
|
+
language: "en-US" as const,
|
|
641
|
+
detailedDescription: true,
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
646
|
+
|
|
647
|
+
const mockFetch = vi.mocked(fetch);
|
|
648
|
+
mockFetch.mockResolvedValue({
|
|
649
|
+
ok: true,
|
|
650
|
+
json: async () => ({
|
|
651
|
+
choices: [
|
|
652
|
+
{
|
|
653
|
+
message: {
|
|
654
|
+
content: "feat(auth): add user login functionality\n\n- Implement username/password login API\n- Add login status validation middleware",
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
}),
|
|
659
|
+
} as Response);
|
|
660
|
+
|
|
661
|
+
const result = await generateAICommitMessage(enDetailedConfig);
|
|
662
|
+
|
|
663
|
+
expect(result).toContain("feat(auth): add user login functionality");
|
|
664
|
+
expect(result).toContain("- Implement username/password login API");
|
|
665
|
+
|
|
666
|
+
// 验证 prompt 使用英文指令
|
|
667
|
+
const callArgs = mockFetch.mock.calls[0][1] as RequestInit;
|
|
668
|
+
const body = JSON.parse(callArgs.body as string);
|
|
669
|
+
const prompt = body.messages[0].content;
|
|
670
|
+
|
|
671
|
+
expect(prompt).toContain("Detailed description: List main changes");
|
|
672
|
+
expect(prompt).toContain("starting with \"- \"");
|
|
673
|
+
expect(prompt).not.toContain("详细描述");
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
describe("Ollama 特殊情况", () => {
|
|
678
|
+
it("Ollama 连接失败时应该提供安装提示", async () => {
|
|
679
|
+
const ollamaConfig: GwConfig = {
|
|
680
|
+
featurePrefix: "feature",
|
|
681
|
+
hotfixPrefix: "hotfix",
|
|
682
|
+
requireId: false,
|
|
683
|
+
featureIdLabel: "Story ID",
|
|
684
|
+
hotfixIdLabel: "Issue ID",
|
|
685
|
+
aiCommit: {
|
|
686
|
+
enabled: true,
|
|
687
|
+
provider: "ollama",
|
|
688
|
+
model: "qwen2.5-coder:7b",
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
vi.mocked(execOutput).mockReturnValue("diff --git a/file.ts b/file.ts");
|
|
693
|
+
|
|
694
|
+
const mockFetch = vi.mocked(fetch);
|
|
695
|
+
mockFetch.mockRejectedValue(new Error("Connection refused"));
|
|
696
|
+
|
|
697
|
+
await expect(generateAICommitMessage(ollamaConfig)).rejects.toThrow(
|
|
698
|
+
"Ollama 连接失败"
|
|
699
|
+
);
|
|
700
|
+
await expect(generateAICommitMessage(ollamaConfig)).rejects.toThrow(
|
|
701
|
+
"ollama.com"
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
});
|