@zjex/git-workflow 0.4.7 → 0.5.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/CHANGELOG.md +6 -0
- package/README.md +44 -6
- package/dist/index.js +614 -11
- package/docs/.vitepress/cache/deps/_metadata.json +10 -10
- package/docs/.vitepress/config.ts +2 -0
- package/docs/commands/index.md +4 -0
- package/docs/commands/review.md +142 -0
- package/docs/guide/ai-review.md +159 -0
- package/docs/guide/index.md +2 -0
- package/docs/index.md +26 -3
- package/package.json +1 -1
- package/src/commands/review.ts +759 -0
- package/src/index.ts +29 -1
- package/tests/review.test.ts +1058 -0
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-2CLQ7TTZ.js +0 -9719
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-2CLQ7TTZ.js.map +0 -7
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-LE5NDSFD.js +0 -12824
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-LE5NDSFD.js.map +0 -7
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/package.json +0 -3
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vue_devtools-api.js +0 -4505
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vue_devtools-api.js.map +0 -7
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_core.js +0 -583
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_core.js.map +0 -7
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_integrations_useFocusTrap.js +0 -1352
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_integrations_useFocusTrap.js.map +0 -7
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___mark__js_src_vanilla__js.js +0 -1665
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___mark__js_src_vanilla__js.js.map +0 -7
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___minisearch.js +0 -1813
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___minisearch.js.map +0 -7
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vue.js +0 -347
- package/docs/.vitepress/cache/deps_temp_44e2fb0f/vue.js.map +0 -7
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execOutput } from "../src/utils";
|
|
3
|
+
|
|
4
|
+
// Mock dependencies
|
|
5
|
+
vi.mock("child_process", () => ({
|
|
6
|
+
execSync: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("../src/utils", () => ({
|
|
10
|
+
execOutput: vi.fn(),
|
|
11
|
+
colors: {
|
|
12
|
+
red: (s: string) => s,
|
|
13
|
+
green: (s: string) => s,
|
|
14
|
+
yellow: (s: string) => s,
|
|
15
|
+
cyan: (s: string) => s,
|
|
16
|
+
dim: (s: string) => s,
|
|
17
|
+
},
|
|
18
|
+
theme: {},
|
|
19
|
+
divider: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("../src/config", () => ({
|
|
23
|
+
loadConfig: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("fs", () => ({
|
|
27
|
+
writeFileSync: vi.fn(),
|
|
28
|
+
existsSync: vi.fn(),
|
|
29
|
+
mkdirSync: vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("@inquirer/prompts", () => ({
|
|
33
|
+
select: vi.fn(),
|
|
34
|
+
checkbox: vi.fn(),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock("ora", () => ({
|
|
38
|
+
default: vi.fn(() => ({
|
|
39
|
+
start: vi.fn().mockReturnThis(),
|
|
40
|
+
succeed: vi.fn().mockReturnThis(),
|
|
41
|
+
fail: vi.fn().mockReturnThis(),
|
|
42
|
+
})),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
global.fetch = vi.fn();
|
|
46
|
+
|
|
47
|
+
describe("Review 功能测试", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("Commit 信息解析", () => {
|
|
57
|
+
it("应该正确解析 git log 输出", () => {
|
|
58
|
+
const output =
|
|
59
|
+
"abc123def|abc123d|feat: 添加新功能|张三|2024-01-20";
|
|
60
|
+
const [hash, shortHash, subject, author, date] = output.split("|");
|
|
61
|
+
|
|
62
|
+
expect(hash).toBe("abc123def");
|
|
63
|
+
expect(shortHash).toBe("abc123d");
|
|
64
|
+
expect(subject).toBe("feat: 添加新功能");
|
|
65
|
+
expect(author).toBe("张三");
|
|
66
|
+
expect(date).toBe("2024-01-20");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("应该正确解析多个 commits", () => {
|
|
70
|
+
const output = `abc123def|abc123d|feat: 添加新功能|张三|2024-01-20
|
|
71
|
+
def456ghi|def456g|fix: 修复 bug|李四|2024-01-19
|
|
72
|
+
ghi789jkl|ghi789j|docs: 更新文档|王五|2024-01-18`;
|
|
73
|
+
|
|
74
|
+
const commits = output
|
|
75
|
+
.split("\n")
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.map((line) => {
|
|
78
|
+
const [hash, shortHash, subject, author, date] = line.split("|");
|
|
79
|
+
return { hash, shortHash, subject, author, date };
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(commits).toHaveLength(3);
|
|
83
|
+
expect(commits[0].subject).toBe("feat: 添加新功能");
|
|
84
|
+
expect(commits[1].subject).toBe("fix: 修复 bug");
|
|
85
|
+
expect(commits[2].subject).toBe("docs: 更新文档");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("空输出应该返回空数组", () => {
|
|
89
|
+
const output = "";
|
|
90
|
+
const commits = output
|
|
91
|
+
.split("\n")
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.map((line) => {
|
|
94
|
+
const [hash, shortHash, subject, author, date] = line.split("|");
|
|
95
|
+
return { hash, shortHash, subject, author, date };
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(commits).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("Diff 解析", () => {
|
|
103
|
+
it("应该正确解析文件状态 - 新增文件", () => {
|
|
104
|
+
const diff = `diff --git a/src/new.ts b/src/new.ts
|
|
105
|
+
new file mode 100644
|
|
106
|
+
index 0000000..abc1234
|
|
107
|
+
--- /dev/null
|
|
108
|
+
+++ b/src/new.ts
|
|
109
|
+
@@ -0,0 +1,10 @@
|
|
110
|
+
+export function newFunction() {
|
|
111
|
+
+ return "hello";
|
|
112
|
+
+}`;
|
|
113
|
+
|
|
114
|
+
const isNewFile = diff.includes("new file mode");
|
|
115
|
+
expect(isNewFile).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("应该正确解析文件状态 - 删除文件", () => {
|
|
119
|
+
const diff = `diff --git a/src/old.ts b/src/old.ts
|
|
120
|
+
deleted file mode 100644
|
|
121
|
+
index abc1234..0000000
|
|
122
|
+
--- a/src/old.ts
|
|
123
|
+
+++ /dev/null
|
|
124
|
+
@@ -1,10 +0,0 @@
|
|
125
|
+
-export function oldFunction() {
|
|
126
|
+
- return "goodbye";
|
|
127
|
+
-}`;
|
|
128
|
+
|
|
129
|
+
const isDeletedFile = diff.includes("deleted file mode");
|
|
130
|
+
expect(isDeletedFile).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("应该正确解析文件状态 - 重命名文件", () => {
|
|
134
|
+
const diff = `diff --git a/src/old.ts b/src/new.ts
|
|
135
|
+
rename from src/old.ts
|
|
136
|
+
rename to src/new.ts`;
|
|
137
|
+
|
|
138
|
+
const isRenamed = diff.includes("rename from");
|
|
139
|
+
expect(isRenamed).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("应该正确解析文件状态 - 修改文件", () => {
|
|
143
|
+
const diff = `diff --git a/src/utils.ts b/src/utils.ts
|
|
144
|
+
index abc1234..def5678 100644
|
|
145
|
+
--- a/src/utils.ts
|
|
146
|
+
+++ b/src/utils.ts
|
|
147
|
+
@@ -10,6 +10,8 @@ export function helper() {
|
|
148
|
+
return "helper";
|
|
149
|
+
}
|
|
150
|
+
+
|
|
151
|
+
+export function newHelper() {
|
|
152
|
+
+ return "new helper";
|
|
153
|
+
+}`;
|
|
154
|
+
|
|
155
|
+
const isNewFile = diff.includes("new file mode");
|
|
156
|
+
const isDeletedFile = diff.includes("deleted file mode");
|
|
157
|
+
const isRenamed = diff.includes("rename from");
|
|
158
|
+
const isModified = !isNewFile && !isDeletedFile && !isRenamed;
|
|
159
|
+
|
|
160
|
+
expect(isModified).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("应该正确提取文件路径", () => {
|
|
164
|
+
const diffHeader = "diff --git a/src/utils.ts b/src/utils.ts";
|
|
165
|
+
const match = diffHeader.match(/a\/(.+) b\/(.+)/);
|
|
166
|
+
|
|
167
|
+
expect(match).not.toBeNull();
|
|
168
|
+
expect(match![1]).toBe("src/utils.ts");
|
|
169
|
+
expect(match![2]).toBe("src/utils.ts");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("应该正确分割多个文件的 diff", () => {
|
|
173
|
+
const diff = `diff --git a/src/file1.ts b/src/file1.ts
|
|
174
|
+
index abc1234..def5678 100644
|
|
175
|
+
--- a/src/file1.ts
|
|
176
|
+
+++ b/src/file1.ts
|
|
177
|
+
@@ -1,3 +1,4 @@
|
|
178
|
+
+// new line
|
|
179
|
+
export const a = 1;
|
|
180
|
+
diff --git a/src/file2.ts b/src/file2.ts
|
|
181
|
+
index ghi9012..jkl3456 100644
|
|
182
|
+
--- a/src/file2.ts
|
|
183
|
+
+++ b/src/file2.ts
|
|
184
|
+
@@ -1,3 +1,4 @@
|
|
185
|
+
+// another new line
|
|
186
|
+
export const b = 2;`;
|
|
187
|
+
|
|
188
|
+
const fileDiffs = diff.split(/^diff --git /m).filter(Boolean);
|
|
189
|
+
expect(fileDiffs).toHaveLength(2);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("Diff 统计", () => {
|
|
194
|
+
it("应该正确统计新增行数", () => {
|
|
195
|
+
const diff = `@@ -1,3 +1,6 @@
|
|
196
|
+
existing line
|
|
197
|
+
+new line 1
|
|
198
|
+
+new line 2
|
|
199
|
+
+new line 3
|
|
200
|
+
another existing line`;
|
|
201
|
+
|
|
202
|
+
const lines = diff.split("\n");
|
|
203
|
+
let additions = 0;
|
|
204
|
+
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
207
|
+
additions++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
expect(additions).toBe(3);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("应该正确统计删除行数", () => {
|
|
215
|
+
const diff = `@@ -1,6 +1,3 @@
|
|
216
|
+
existing line
|
|
217
|
+
-deleted line 1
|
|
218
|
+
-deleted line 2
|
|
219
|
+
-deleted line 3
|
|
220
|
+
another existing line`;
|
|
221
|
+
|
|
222
|
+
const lines = diff.split("\n");
|
|
223
|
+
let deletions = 0;
|
|
224
|
+
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
227
|
+
deletions++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(deletions).toBe(3);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("应该正确统计文件数", () => {
|
|
235
|
+
const diff = `diff --git a/src/file1.ts b/src/file1.ts
|
|
236
|
+
+new line
|
|
237
|
+
diff --git a/src/file2.ts b/src/file2.ts
|
|
238
|
+
+another new line
|
|
239
|
+
diff --git a/src/file3.ts b/src/file3.ts
|
|
240
|
+
+third new line`;
|
|
241
|
+
|
|
242
|
+
const fileDiffs = diff.split(/^diff --git /m).filter(Boolean);
|
|
243
|
+
expect(fileDiffs).toHaveLength(3);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("应该忽略 +++ 和 --- 行", () => {
|
|
247
|
+
const diff = `--- a/src/file.ts
|
|
248
|
+
+++ b/src/file.ts
|
|
249
|
+
@@ -1,3 +1,4 @@
|
|
250
|
+
+real new line`;
|
|
251
|
+
|
|
252
|
+
const lines = diff.split("\n");
|
|
253
|
+
let additions = 0;
|
|
254
|
+
let deletions = 0;
|
|
255
|
+
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
258
|
+
additions++;
|
|
259
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
260
|
+
deletions++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
expect(additions).toBe(1);
|
|
265
|
+
expect(deletions).toBe(0);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("问题严重程度", () => {
|
|
270
|
+
const severityLevels = [
|
|
271
|
+
{ level: "critical", emoji: "🔴", description: "严重问题" },
|
|
272
|
+
{ level: "warning", emoji: "🟡", description: "警告" },
|
|
273
|
+
{ level: "suggestion", emoji: "🔵", description: "建议" },
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
it("应该有 3 种严重程度", () => {
|
|
277
|
+
expect(severityLevels).toHaveLength(3);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("每种严重程度都应该有必需的字段", () => {
|
|
281
|
+
severityLevels.forEach((level) => {
|
|
282
|
+
expect(level).toHaveProperty("level");
|
|
283
|
+
expect(level).toHaveProperty("emoji");
|
|
284
|
+
expect(level).toHaveProperty("description");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("审查维度", () => {
|
|
290
|
+
const reviewDimensions = [
|
|
291
|
+
{ name: "代码质量", key: "quality" },
|
|
292
|
+
{ name: "潜在 Bug", key: "bugs" },
|
|
293
|
+
{ name: "安全问题", key: "security" },
|
|
294
|
+
{ name: "性能问题", key: "performance" },
|
|
295
|
+
{ name: "最佳实践", key: "bestPractices" },
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
it("应该有 5 个审查维度", () => {
|
|
299
|
+
expect(reviewDimensions).toHaveLength(5);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("每个维度都应该有名称和键", () => {
|
|
303
|
+
reviewDimensions.forEach((dim) => {
|
|
304
|
+
expect(dim.name).toBeTruthy();
|
|
305
|
+
expect(dim.key).toBeTruthy();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("报告生成", () => {
|
|
311
|
+
it("应该生成正确的文件名格式", () => {
|
|
312
|
+
const commitHash = "abc1234";
|
|
313
|
+
const timestamp = "2024-01-20T10-30-00";
|
|
314
|
+
const filename = `review-${commitHash}-${timestamp}.md`;
|
|
315
|
+
|
|
316
|
+
expect(filename).toBe("review-abc1234-2024-01-20T10-30-00.md");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("多个 commit 应该用连字符连接", () => {
|
|
320
|
+
const commits = ["abc1234", "def5678", "ghi9012"];
|
|
321
|
+
const commitInfo = commits.join("-");
|
|
322
|
+
|
|
323
|
+
expect(commitInfo).toBe("abc1234-def5678-ghi9012");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("暂存区审查应该使用 staged 作为标识", () => {
|
|
327
|
+
const commits: string[] = [];
|
|
328
|
+
const commitInfo = commits.length > 0 ? commits.join("-") : "staged";
|
|
329
|
+
|
|
330
|
+
expect(commitInfo).toBe("staged");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("报告目录应该是 .gw-reviews", () => {
|
|
334
|
+
const reviewDir = ".gw-reviews";
|
|
335
|
+
expect(reviewDir).toBe(".gw-reviews");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("AI 提供商配置", () => {
|
|
340
|
+
const providers = [
|
|
341
|
+
{ id: "github", name: "GitHub Models", defaultModel: "gpt-4o" },
|
|
342
|
+
{ id: "openai", name: "OpenAI", defaultModel: "gpt-4o" },
|
|
343
|
+
{ id: "claude", name: "Claude", defaultModel: "claude-3-5-sonnet-20241022" },
|
|
344
|
+
{ id: "ollama", name: "Ollama", defaultModel: "qwen2.5-coder:14b" },
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
it("应该支持 4 种 AI 提供商", () => {
|
|
348
|
+
expect(providers).toHaveLength(4);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("每个提供商都应该有默认模型", () => {
|
|
352
|
+
providers.forEach((provider) => {
|
|
353
|
+
expect(provider.defaultModel).toBeTruthy();
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("Ollama 应该使用本地端点", () => {
|
|
358
|
+
const ollamaEndpoint = "http://localhost:11434/api/generate";
|
|
359
|
+
expect(ollamaEndpoint).toContain("localhost");
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("Diff 长度限制", () => {
|
|
364
|
+
it("应该截断过长的 diff", () => {
|
|
365
|
+
const maxLength = 30000;
|
|
366
|
+
const longDiff = "a".repeat(35000);
|
|
367
|
+
|
|
368
|
+
const truncated =
|
|
369
|
+
longDiff.length > maxLength
|
|
370
|
+
? longDiff.slice(0, maxLength) + "\n\n[... diff 内容过长,已截断 ...]"
|
|
371
|
+
: longDiff;
|
|
372
|
+
|
|
373
|
+
expect(truncated.length).toBeLessThan(longDiff.length);
|
|
374
|
+
expect(truncated).toContain("已截断");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("短 diff 不应该被截断", () => {
|
|
378
|
+
const maxLength = 30000;
|
|
379
|
+
const shortDiff = "a".repeat(1000);
|
|
380
|
+
|
|
381
|
+
const result =
|
|
382
|
+
shortDiff.length > maxLength
|
|
383
|
+
? shortDiff.slice(0, maxLength) + "\n\n[... diff 内容过长,已截断 ...]"
|
|
384
|
+
: shortDiff;
|
|
385
|
+
|
|
386
|
+
expect(result).toBe(shortDiff);
|
|
387
|
+
expect(result).not.toContain("已截断");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("命令选项", () => {
|
|
392
|
+
it("--last 选项应该限制 commit 数量", () => {
|
|
393
|
+
const lastOption = 5;
|
|
394
|
+
const allCommits = Array.from({ length: 20 }, (_, i) => ({
|
|
395
|
+
hash: `hash${i}`,
|
|
396
|
+
subject: `commit ${i}`,
|
|
397
|
+
}));
|
|
398
|
+
|
|
399
|
+
const limitedCommits = allCommits.slice(0, lastOption);
|
|
400
|
+
expect(limitedCommits).toHaveLength(5);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("--staged 选项应该审查暂存区", () => {
|
|
404
|
+
const options = { staged: true };
|
|
405
|
+
expect(options.staged).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("--output 选项应该指定输出路径", () => {
|
|
409
|
+
const options = { output: "./my-review.md" };
|
|
410
|
+
expect(options.output).toBe("./my-review.md");
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("交互式选择", () => {
|
|
415
|
+
it("选项应该包含暂存区(如果有更改)", () => {
|
|
416
|
+
const hasStagedChanges = true;
|
|
417
|
+
const choices: any[] = [];
|
|
418
|
+
|
|
419
|
+
if (hasStagedChanges) {
|
|
420
|
+
choices.push({
|
|
421
|
+
name: "📦 暂存区的更改 (staged changes)",
|
|
422
|
+
value: "staged",
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
expect(choices).toHaveLength(1);
|
|
427
|
+
expect(choices[0].value).toBe("staged");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("选项应该包含最近的 commits", () => {
|
|
431
|
+
const recentCommits = [
|
|
432
|
+
{ hash: "abc123", shortHash: "abc123", subject: "feat: 新功能" },
|
|
433
|
+
{ hash: "def456", shortHash: "def456", subject: "fix: 修复 bug" },
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
const choices = recentCommits.map((c) => ({
|
|
437
|
+
name: `${c.shortHash} ${c.subject}`,
|
|
438
|
+
value: c.hash,
|
|
439
|
+
}));
|
|
440
|
+
|
|
441
|
+
expect(choices).toHaveLength(2);
|
|
442
|
+
expect(choices[0].value).toBe("abc123");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("没有可审查内容时应该返回空数组", () => {
|
|
446
|
+
const hasStagedChanges = false;
|
|
447
|
+
const recentCommits: any[] = [];
|
|
448
|
+
const choices: any[] = [];
|
|
449
|
+
|
|
450
|
+
if (hasStagedChanges) {
|
|
451
|
+
choices.push({ name: "staged", value: "staged" });
|
|
452
|
+
}
|
|
453
|
+
choices.push(...recentCommits);
|
|
454
|
+
|
|
455
|
+
expect(choices).toHaveLength(0);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("系统提示词", () => {
|
|
460
|
+
it("中文提示词应该包含审查原则", () => {
|
|
461
|
+
const zhPrompt = `你是一个资深的代码审查专家
|
|
462
|
+
## 审查原则
|
|
463
|
+
1. 重点关注变更代码
|
|
464
|
+
2. 提供具体建议
|
|
465
|
+
3. 区分问题严重程度`;
|
|
466
|
+
|
|
467
|
+
expect(zhPrompt).toContain("审查原则");
|
|
468
|
+
expect(zhPrompt).toContain("重点关注变更代码");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("英文提示词应该包含 Review Principles", () => {
|
|
472
|
+
const enPrompt = `You are a senior code review expert
|
|
473
|
+
## Review Principles
|
|
474
|
+
1. Focus on Changed Code
|
|
475
|
+
2. Provide Specific Suggestions
|
|
476
|
+
3. Categorize Issue Severity`;
|
|
477
|
+
|
|
478
|
+
expect(enPrompt).toContain("Review Principles");
|
|
479
|
+
expect(enPrompt).toContain("Focus on Changed Code");
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("提示词应该包含 diff 格式说明", () => {
|
|
483
|
+
const prompt = `## Diff 格式说明
|
|
484
|
+
- 以 + 开头的行是新增的代码
|
|
485
|
+
- 以 - 开头的行是删除的代码
|
|
486
|
+
- @@ 行表示代码位置信息`;
|
|
487
|
+
|
|
488
|
+
expect(prompt).toContain("Diff 格式说明");
|
|
489
|
+
expect(prompt).toContain("新增的代码");
|
|
490
|
+
expect(prompt).toContain("删除的代码");
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("用户提示词", () => {
|
|
495
|
+
it("应该包含变更概览", () => {
|
|
496
|
+
const stats = { files: 3, additions: 45, deletions: 12 };
|
|
497
|
+
const prompt = `## 变更概览
|
|
498
|
+
- 涉及文件: ${stats.files} 个
|
|
499
|
+
- 新增行数: +${stats.additions}
|
|
500
|
+
- 删除行数: -${stats.deletions}`;
|
|
501
|
+
|
|
502
|
+
expect(prompt).toContain("变更概览");
|
|
503
|
+
expect(prompt).toContain("3 个");
|
|
504
|
+
expect(prompt).toContain("+45");
|
|
505
|
+
expect(prompt).toContain("-12");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("应该包含相关提交信息", () => {
|
|
509
|
+
const commits = [
|
|
510
|
+
{ shortHash: "abc123", subject: "feat: 新功能", author: "张三", date: "2024-01-20" },
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
let prompt = "## 相关提交\n\n";
|
|
514
|
+
for (const commit of commits) {
|
|
515
|
+
prompt += `- \`${commit.shortHash}\` ${commit.subject} (${commit.author}, ${commit.date})\n`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
expect(prompt).toContain("相关提交");
|
|
519
|
+
expect(prompt).toContain("abc123");
|
|
520
|
+
expect(prompt).toContain("feat: 新功能");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("应该包含变更文件列表", () => {
|
|
524
|
+
const files = [
|
|
525
|
+
{ newPath: "src/new.ts", status: "A" },
|
|
526
|
+
{ newPath: "src/modified.ts", status: "M" },
|
|
527
|
+
{ newPath: "src/deleted.ts", status: "D" },
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
let prompt = "## 变更文件列表\n\n";
|
|
531
|
+
for (const file of files) {
|
|
532
|
+
const statusIcon =
|
|
533
|
+
file.status === "A" ? "🆕" : file.status === "D" ? "🗑️" : "✏️";
|
|
534
|
+
prompt += `- ${statusIcon} \`${file.newPath}\`\n`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
expect(prompt).toContain("🆕");
|
|
538
|
+
expect(prompt).toContain("🗑️");
|
|
539
|
+
expect(prompt).toContain("✏️");
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
describe("报告内容", () => {
|
|
544
|
+
it("报告应该包含标题", () => {
|
|
545
|
+
const report = "# 🔍 代码审查报告\n\n";
|
|
546
|
+
expect(report).toContain("代码审查报告");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("报告应该包含生成时间", () => {
|
|
550
|
+
const timestamp = new Date().toLocaleString("zh-CN");
|
|
551
|
+
const report = `> 生成时间: ${timestamp}\n\n`;
|
|
552
|
+
expect(report).toContain("生成时间");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("报告应该包含变更统计表格", () => {
|
|
556
|
+
const stats = { files: 3, additions: 45, deletions: 12 };
|
|
557
|
+
const report = `## 📊 变更统计
|
|
558
|
+
|
|
559
|
+
| 指标 | 数值 |
|
|
560
|
+
|------|------|\n| 文件数 | ${stats.files} |
|
|
561
|
+
| 新增行 | +${stats.additions} |
|
|
562
|
+
| 删除行 | -${stats.deletions} |`;
|
|
563
|
+
|
|
564
|
+
expect(report).toContain("变更统计");
|
|
565
|
+
expect(report).toContain("| 文件数 | 3 |");
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("报告应该包含 AI 审查结果", () => {
|
|
569
|
+
const reviewContent = "### 概述\n本次变更主要添加了用户登录功能...";
|
|
570
|
+
const report = `## 🤖 AI 审查结果\n\n${reviewContent}`;
|
|
571
|
+
|
|
572
|
+
expect(report).toContain("AI 审查结果");
|
|
573
|
+
expect(report).toContain("概述");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("报告应该包含工具署名", () => {
|
|
577
|
+
const footer = "*本报告由 [git-workflow](https://github.com/iamzjt-front-end/git-workflow) 的 AI Review 功能生成*";
|
|
578
|
+
expect(footer).toContain("git-workflow");
|
|
579
|
+
expect(footer).toContain("AI Review");
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
describe("getRecentCommits 函数逻辑", () => {
|
|
584
|
+
it("应该使用正确的 git log 格式", () => {
|
|
585
|
+
const limit = 20;
|
|
586
|
+
const expectedCommand = `git log -${limit} --pretty=format:"%H|%h|%s|%an|%ad" --date=short`;
|
|
587
|
+
expect(expectedCommand).toContain("--pretty=format");
|
|
588
|
+
expect(expectedCommand).toContain("%H|%h|%s|%an|%ad");
|
|
589
|
+
expect(expectedCommand).toContain("--date=short");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("应该正确处理 limit 参数", () => {
|
|
593
|
+
const limits = [5, 10, 20, 50];
|
|
594
|
+
limits.forEach((limit) => {
|
|
595
|
+
const command = `git log -${limit} --pretty=format:"%H|%h|%s|%an|%ad" --date=short`;
|
|
596
|
+
expect(command).toContain(`-${limit}`);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("git log 失败时应该返回空数组", () => {
|
|
601
|
+
const result: any[] = [];
|
|
602
|
+
try {
|
|
603
|
+
throw new Error("git log failed");
|
|
604
|
+
} catch {
|
|
605
|
+
// 返回空数组
|
|
606
|
+
}
|
|
607
|
+
expect(result).toHaveLength(0);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
describe("getStagedDiff 函数逻辑", () => {
|
|
612
|
+
it("应该优先获取暂存区的 diff", () => {
|
|
613
|
+
const stagedDiff = "diff --git a/file.ts b/file.ts\n+new line";
|
|
614
|
+
const workingDiff = "diff --git a/other.ts b/other.ts\n+other line";
|
|
615
|
+
|
|
616
|
+
// 模拟逻辑:如果有暂存区 diff,返回暂存区 diff
|
|
617
|
+
const result = stagedDiff || workingDiff;
|
|
618
|
+
expect(result).toBe(stagedDiff);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("暂存区为空时应该获取工作区 diff", () => {
|
|
622
|
+
const stagedDiff = "";
|
|
623
|
+
const workingDiff = "diff --git a/other.ts b/other.ts\n+other line";
|
|
624
|
+
|
|
625
|
+
const result = stagedDiff || workingDiff;
|
|
626
|
+
expect(result).toBe(workingDiff);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("两者都为空时应该返回空字符串", () => {
|
|
630
|
+
const stagedDiff = "";
|
|
631
|
+
const workingDiff = "";
|
|
632
|
+
|
|
633
|
+
const result = stagedDiff || workingDiff || "";
|
|
634
|
+
expect(result).toBe("");
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
describe("getCommitDiff 函数逻辑", () => {
|
|
639
|
+
it("应该使用正确的 git show 命令", () => {
|
|
640
|
+
const hash = "abc1234";
|
|
641
|
+
const expectedCommand = `git show ${hash} --format="" --patch`;
|
|
642
|
+
expect(expectedCommand).toContain("git show");
|
|
643
|
+
expect(expectedCommand).toContain(hash);
|
|
644
|
+
expect(expectedCommand).toContain("--format=\"\"");
|
|
645
|
+
expect(expectedCommand).toContain("--patch");
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
describe("getMultipleCommitsDiff 函数逻辑", () => {
|
|
650
|
+
it("空数组应该返回空字符串", () => {
|
|
651
|
+
const hashes: string[] = [];
|
|
652
|
+
const result = hashes.length === 0 ? "" : "some diff";
|
|
653
|
+
expect(result).toBe("");
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("单个 hash 应该调用 getCommitDiff", () => {
|
|
657
|
+
const hashes = ["abc1234"];
|
|
658
|
+
const isSingle = hashes.length === 1;
|
|
659
|
+
expect(isSingle).toBe(true);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("多个 hash 应该使用范围 diff", () => {
|
|
663
|
+
const hashes = ["abc1234", "def5678", "ghi9012"];
|
|
664
|
+
const oldest = hashes[hashes.length - 1];
|
|
665
|
+
const newest = hashes[0];
|
|
666
|
+
const rangeCommand = `git diff ${oldest}^..${newest}`;
|
|
667
|
+
|
|
668
|
+
expect(rangeCommand).toContain("git diff");
|
|
669
|
+
expect(rangeCommand).toContain("ghi9012^");
|
|
670
|
+
expect(rangeCommand).toContain("abc1234");
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
describe("parseDiff 函数逻辑", () => {
|
|
675
|
+
it("应该正确解析完整的 diff", () => {
|
|
676
|
+
const diff = `diff --git a/src/utils.ts b/src/utils.ts
|
|
677
|
+
index abc1234..def5678 100644
|
|
678
|
+
--- a/src/utils.ts
|
|
679
|
+
+++ b/src/utils.ts
|
|
680
|
+
@@ -1,3 +1,4 @@
|
|
681
|
+
+// new comment
|
|
682
|
+
export const a = 1;`;
|
|
683
|
+
|
|
684
|
+
const fileDiffs = diff.split(/^diff --git /m).filter(Boolean);
|
|
685
|
+
expect(fileDiffs).toHaveLength(1);
|
|
686
|
+
|
|
687
|
+
const firstDiff = fileDiffs[0];
|
|
688
|
+
const headerMatch = firstDiff.split("\n")[0]?.match(/a\/(.+) b\/(.+)/);
|
|
689
|
+
expect(headerMatch).not.toBeNull();
|
|
690
|
+
expect(headerMatch![1]).toBe("src/utils.ts");
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("应该处理没有匹配的 header", () => {
|
|
694
|
+
const invalidDiff = "some invalid content";
|
|
695
|
+
const headerMatch = invalidDiff.match(/a\/(.+) b\/(.+)/);
|
|
696
|
+
expect(headerMatch).toBeNull();
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
describe("AI API 调用逻辑", () => {
|
|
701
|
+
it("GitHub API 应该使用正确的 endpoint", () => {
|
|
702
|
+
const endpoint = "https://models.github.ai/inference/chat/completions";
|
|
703
|
+
expect(endpoint).toContain("github.ai");
|
|
704
|
+
expect(endpoint).toContain("chat/completions");
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("OpenAI API 应该使用正确的 endpoint", () => {
|
|
708
|
+
const endpoint = "https://api.openai.com/v1/chat/completions";
|
|
709
|
+
expect(endpoint).toContain("openai.com");
|
|
710
|
+
expect(endpoint).toContain("chat/completions");
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("Claude API 应该使用正确的 endpoint", () => {
|
|
714
|
+
const endpoint = "https://api.anthropic.com/v1/messages";
|
|
715
|
+
expect(endpoint).toContain("anthropic.com");
|
|
716
|
+
expect(endpoint).toContain("messages");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("Ollama API 应该使用本地 endpoint", () => {
|
|
720
|
+
const endpoint = "http://localhost:11434/api/generate";
|
|
721
|
+
expect(endpoint).toContain("localhost:11434");
|
|
722
|
+
expect(endpoint).toContain("generate");
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("API 请求应该包含正确的 headers", () => {
|
|
726
|
+
const githubHeaders = {
|
|
727
|
+
Authorization: "Bearer test-key",
|
|
728
|
+
"Content-Type": "application/json",
|
|
729
|
+
};
|
|
730
|
+
expect(githubHeaders.Authorization).toContain("Bearer");
|
|
731
|
+
expect(githubHeaders["Content-Type"]).toBe("application/json");
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it("Claude API 应该使用 x-api-key header", () => {
|
|
735
|
+
const claudeHeaders = {
|
|
736
|
+
"x-api-key": "test-key",
|
|
737
|
+
"anthropic-version": "2023-06-01",
|
|
738
|
+
"Content-Type": "application/json",
|
|
739
|
+
};
|
|
740
|
+
expect(claudeHeaders["x-api-key"]).toBe("test-key");
|
|
741
|
+
expect(claudeHeaders["anthropic-version"]).toBe("2023-06-01");
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("API 请求应该包含正确的 body 结构", () => {
|
|
745
|
+
const body = {
|
|
746
|
+
model: "gpt-4o",
|
|
747
|
+
messages: [
|
|
748
|
+
{ role: "system", content: "system prompt" },
|
|
749
|
+
{ role: "user", content: "user prompt" },
|
|
750
|
+
],
|
|
751
|
+
max_tokens: 4000,
|
|
752
|
+
temperature: 0.3,
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
expect(body.model).toBe("gpt-4o");
|
|
756
|
+
expect(body.messages).toHaveLength(2);
|
|
757
|
+
expect(body.max_tokens).toBe(4000);
|
|
758
|
+
expect(body.temperature).toBe(0.3);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("Claude API body 结构应该不同", () => {
|
|
762
|
+
const claudeBody = {
|
|
763
|
+
model: "claude-3-5-sonnet-20241022",
|
|
764
|
+
system: "system prompt",
|
|
765
|
+
messages: [{ role: "user", content: "user prompt" }],
|
|
766
|
+
max_tokens: 4000,
|
|
767
|
+
temperature: 0.3,
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
expect(claudeBody.system).toBe("system prompt");
|
|
771
|
+
expect(claudeBody.messages).toHaveLength(1);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it("Ollama API body 结构应该不同", () => {
|
|
775
|
+
const ollamaBody = {
|
|
776
|
+
model: "qwen2.5-coder:14b",
|
|
777
|
+
prompt: "system prompt\n\nuser prompt",
|
|
778
|
+
stream: false,
|
|
779
|
+
options: {
|
|
780
|
+
num_predict: 4000,
|
|
781
|
+
temperature: 0.3,
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
expect(ollamaBody.prompt).toContain("system prompt");
|
|
786
|
+
expect(ollamaBody.stream).toBe(false);
|
|
787
|
+
expect(ollamaBody.options.num_predict).toBe(4000);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
describe("错误处理", () => {
|
|
792
|
+
it("不支持的 AI 提供商应该抛出错误", () => {
|
|
793
|
+
const provider = "invalid";
|
|
794
|
+
const providers = ["github", "openai", "claude", "ollama"];
|
|
795
|
+
const isSupported = providers.includes(provider);
|
|
796
|
+
|
|
797
|
+
expect(isSupported).toBe(false);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it("缺少 API key 时应该抛出错误(非 Ollama)", () => {
|
|
801
|
+
const provider = "github";
|
|
802
|
+
const apiKey = "";
|
|
803
|
+
const needsKey = provider !== "ollama" && !apiKey;
|
|
804
|
+
|
|
805
|
+
expect(needsKey).toBe(true);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("Ollama 不需要 API key", () => {
|
|
809
|
+
const provider = "ollama";
|
|
810
|
+
const apiKey = "";
|
|
811
|
+
const needsKey = provider !== "ollama" && !apiKey;
|
|
812
|
+
|
|
813
|
+
expect(needsKey).toBe(false);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it("API 响应失败时应该抛出错误", () => {
|
|
817
|
+
const response = { ok: false, status: 401 };
|
|
818
|
+
expect(response.ok).toBe(false);
|
|
819
|
+
expect(response.status).toBe(401);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("找不到 commit 时应该退出", () => {
|
|
823
|
+
const commitInfo = "";
|
|
824
|
+
const shouldExit = !commitInfo;
|
|
825
|
+
expect(shouldExit).toBe(true);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it("没有代码变更时应该提示", () => {
|
|
829
|
+
const diff = "";
|
|
830
|
+
const hasChanges = !!diff;
|
|
831
|
+
expect(hasChanges).toBe(false);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it("未选择任何内容时应该提示", () => {
|
|
835
|
+
const selected: string[] = [];
|
|
836
|
+
const hasSelection = selected.length > 0;
|
|
837
|
+
expect(hasSelection).toBe(false);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
describe("配置检查", () => {
|
|
842
|
+
it("未配置 AI 时应该提示运行 gw init", () => {
|
|
843
|
+
const config = { aiCommit: undefined };
|
|
844
|
+
const hasConfig = !!config.aiCommit?.apiKey;
|
|
845
|
+
expect(hasConfig).toBe(false);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it("配置了 Ollama 但没有 apiKey 应该允许", () => {
|
|
849
|
+
const config = { aiCommit: { provider: "ollama" } };
|
|
850
|
+
const isOllama = config.aiCommit?.provider === "ollama";
|
|
851
|
+
expect(isOllama).toBe(true);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it("应该使用默认语言 zh-CN", () => {
|
|
855
|
+
const config = { aiCommit: {} };
|
|
856
|
+
const language = (config.aiCommit as any).language || "zh-CN";
|
|
857
|
+
expect(language).toBe("zh-CN");
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it("应该使用默认提供商 github", () => {
|
|
861
|
+
const config = { aiCommit: {} };
|
|
862
|
+
const provider = (config.aiCommit as any).provider || "github";
|
|
863
|
+
expect(provider).toBe("github");
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
describe("文件系统操作", () => {
|
|
868
|
+
it("应该在 .gw-reviews 目录不存在时创建", () => {
|
|
869
|
+
const reviewDir = ".gw-reviews";
|
|
870
|
+
const exists = false;
|
|
871
|
+
|
|
872
|
+
if (!exists) {
|
|
873
|
+
// 应该调用 mkdirSync
|
|
874
|
+
expect(reviewDir).toBe(".gw-reviews");
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it("应该使用 recursive: true 创建目录", () => {
|
|
879
|
+
const options = { recursive: true };
|
|
880
|
+
expect(options.recursive).toBe(true);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it("应该使用 utf-8 编码写入文件", () => {
|
|
884
|
+
const encoding = "utf-8";
|
|
885
|
+
expect(encoding).toBe("utf-8");
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
describe("时间戳格式", () => {
|
|
890
|
+
it("应该生成 ISO 格式的时间戳", () => {
|
|
891
|
+
const timestamp = new Date().toISOString();
|
|
892
|
+
expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it("应该替换时间戳中的特殊字符", () => {
|
|
896
|
+
const timestamp = "2024-01-20T10:30:00.000Z";
|
|
897
|
+
const formatted = timestamp.replace(/[:.]/g, "-").slice(0, 19);
|
|
898
|
+
expect(formatted).toBe("2024-01-20T10-30-00");
|
|
899
|
+
expect(formatted).not.toContain(":");
|
|
900
|
+
expect(formatted).not.toContain(".");
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
describe("checkbox 交互配置", () => {
|
|
905
|
+
it("pageSize 应该等于 choices 长度", () => {
|
|
906
|
+
const choices = [
|
|
907
|
+
{ name: "option1", value: "1" },
|
|
908
|
+
{ name: "option2", value: "2" },
|
|
909
|
+
{ name: "option3", value: "3" },
|
|
910
|
+
];
|
|
911
|
+
const pageSize = choices.length;
|
|
912
|
+
expect(pageSize).toBe(3);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it("loop 应该为 false", () => {
|
|
916
|
+
const loop = false;
|
|
917
|
+
expect(loop).toBe(false);
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
describe("select 交互配置", () => {
|
|
922
|
+
it("打开报告选项应该有两个选择", () => {
|
|
923
|
+
const choices = [
|
|
924
|
+
{ name: "是,在编辑器中打开", value: true },
|
|
925
|
+
{ name: "否,稍后查看", value: false },
|
|
926
|
+
];
|
|
927
|
+
expect(choices).toHaveLength(2);
|
|
928
|
+
expect(choices[0].value).toBe(true);
|
|
929
|
+
expect(choices[1].value).toBe(false);
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
describe("文件状态图标", () => {
|
|
934
|
+
it("新增文件应该使用 🆕 图标", () => {
|
|
935
|
+
const status = "A";
|
|
936
|
+
const icon = status === "A" ? "🆕" : status === "D" ? "🗑️" : status === "R" ? "📝" : "✏️";
|
|
937
|
+
expect(icon).toBe("🆕");
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it("删除文件应该使用 🗑️ 图标", () => {
|
|
941
|
+
const status = "D";
|
|
942
|
+
const icon = status === "A" ? "🆕" : status === "D" ? "🗑️" : status === "R" ? "📝" : "✏️";
|
|
943
|
+
expect(icon).toBe("🗑️");
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it("重命名文件应该使用 📝 图标", () => {
|
|
947
|
+
const status = "R";
|
|
948
|
+
const icon = status === "A" ? "🆕" : status === "D" ? "🗑️" : status === "R" ? "📝" : "✏️";
|
|
949
|
+
expect(icon).toBe("📝");
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it("修改文件应该使用 ✏️ 图标", () => {
|
|
953
|
+
const status = "M";
|
|
954
|
+
const icon = status === "A" ? "🆕" : status === "D" ? "🗑️" : status === "R" ? "📝" : "✏️";
|
|
955
|
+
expect(icon).toBe("✏️");
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
describe("英文提示词", () => {
|
|
960
|
+
it("英文变更概览应该使用正确的标题", () => {
|
|
961
|
+
const isZh = false;
|
|
962
|
+
const title = isZh ? "## 变更概览" : "## Change Overview";
|
|
963
|
+
expect(title).toBe("## Change Overview");
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
it("英文相关提交应该使用正确的标题", () => {
|
|
967
|
+
const isZh = false;
|
|
968
|
+
const title = isZh ? "## 相关提交" : "## Related Commits";
|
|
969
|
+
expect(title).toBe("## Related Commits");
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it("英文变更文件列表应该使用正确的标题", () => {
|
|
973
|
+
const isZh = false;
|
|
974
|
+
const title = isZh ? "## 变更文件列表" : "## Changed Files";
|
|
975
|
+
expect(title).toBe("## Changed Files");
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it("英文 Diff 内容应该使用正确的标题", () => {
|
|
979
|
+
const isZh = false;
|
|
980
|
+
const title = isZh ? "## Diff 内容" : "## Diff Content";
|
|
981
|
+
expect(title).toBe("## Diff Content");
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
describe("API 响应解析", () => {
|
|
986
|
+
it("GitHub/OpenAI 响应应该从 choices[0].message.content 获取", () => {
|
|
987
|
+
const response = {
|
|
988
|
+
choices: [
|
|
989
|
+
{
|
|
990
|
+
message: {
|
|
991
|
+
content: "review content",
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
};
|
|
996
|
+
const content = response.choices[0]?.message?.content?.trim() || "";
|
|
997
|
+
expect(content).toBe("review content");
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it("Claude 响应应该从 content[0].text 获取", () => {
|
|
1001
|
+
const response = {
|
|
1002
|
+
content: [
|
|
1003
|
+
{
|
|
1004
|
+
text: "review content",
|
|
1005
|
+
},
|
|
1006
|
+
],
|
|
1007
|
+
};
|
|
1008
|
+
const content = response.content[0]?.text?.trim() || "";
|
|
1009
|
+
expect(content).toBe("review content");
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it("Ollama 响应应该从 response 获取", () => {
|
|
1013
|
+
const response = {
|
|
1014
|
+
response: "review content",
|
|
1015
|
+
};
|
|
1016
|
+
const content = response.response?.trim() || "";
|
|
1017
|
+
expect(content).toBe("review content");
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it("空响应应该返回空字符串", () => {
|
|
1021
|
+
const response = { choices: [] };
|
|
1022
|
+
const content = response.choices[0]?.message?.content?.trim() || "";
|
|
1023
|
+
expect(content).toBe("");
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
describe("Ollama 错误处理", () => {
|
|
1028
|
+
it("连接失败应该提供安装提示", () => {
|
|
1029
|
+
const model = "qwen2.5-coder:14b";
|
|
1030
|
+
const errorMessage = `Ollama 连接失败。请确保:
|
|
1031
|
+
1. 已安装 Ollama (https://ollama.com)
|
|
1032
|
+
2. 运行 'ollama serve'
|
|
1033
|
+
3. 下载模型 'ollama pull ${model}'`;
|
|
1034
|
+
|
|
1035
|
+
expect(errorMessage).toContain("ollama.com");
|
|
1036
|
+
expect(errorMessage).toContain("ollama serve");
|
|
1037
|
+
expect(errorMessage).toContain(`ollama pull ${model}`);
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
describe("spinner 状态", () => {
|
|
1042
|
+
it("开始时应该显示审查中消息", () => {
|
|
1043
|
+
const message = "🤖 AI 正在审查代码...";
|
|
1044
|
+
expect(message).toContain("AI");
|
|
1045
|
+
expect(message).toContain("审查");
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it("成功时应该显示完成消息", () => {
|
|
1049
|
+
const message = "AI 审查完成";
|
|
1050
|
+
expect(message).toContain("完成");
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it("失败时应该显示失败消息", () => {
|
|
1054
|
+
const message = "AI 审查失败";
|
|
1055
|
+
expect(message).toContain("失败");
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
});
|