@spaceflow/review 0.29.1
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 +533 -0
- package/README.md +124 -0
- package/dist/551.js +9 -0
- package/dist/index.js +5704 -0
- package/package.json +50 -0
- package/src/README.md +364 -0
- package/src/__mocks__/@anthropic-ai/claude-agent-sdk.js +3 -0
- package/src/__mocks__/json-stringify-pretty-compact.ts +4 -0
- package/src/deletion-impact.service.spec.ts +974 -0
- package/src/deletion-impact.service.ts +879 -0
- package/src/dto/mcp.dto.ts +42 -0
- package/src/index.ts +32 -0
- package/src/issue-verify.service.spec.ts +460 -0
- package/src/issue-verify.service.ts +309 -0
- package/src/locales/en/review.json +31 -0
- package/src/locales/index.ts +11 -0
- package/src/locales/zh-cn/review.json +31 -0
- package/src/parse-title-options.spec.ts +251 -0
- package/src/parse-title-options.ts +185 -0
- package/src/review-report/formatters/deletion-impact.formatter.ts +144 -0
- package/src/review-report/formatters/index.ts +4 -0
- package/src/review-report/formatters/json.formatter.ts +8 -0
- package/src/review-report/formatters/markdown.formatter.ts +291 -0
- package/src/review-report/formatters/terminal.formatter.ts +130 -0
- package/src/review-report/index.ts +4 -0
- package/src/review-report/review-report.module.ts +8 -0
- package/src/review-report/review-report.service.ts +58 -0
- package/src/review-report/types.ts +26 -0
- package/src/review-spec/index.ts +3 -0
- package/src/review-spec/review-spec.module.ts +10 -0
- package/src/review-spec/review-spec.service.spec.ts +1543 -0
- package/src/review-spec/review-spec.service.ts +902 -0
- package/src/review-spec/types.ts +143 -0
- package/src/review.command.ts +244 -0
- package/src/review.config.ts +58 -0
- package/src/review.mcp.ts +184 -0
- package/src/review.module.ts +52 -0
- package/src/review.service.spec.ts +3007 -0
- package/src/review.service.ts +2603 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +34 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
LlmProxyService,
|
|
4
|
+
type LLMMode,
|
|
5
|
+
type VerboseLevel,
|
|
6
|
+
shouldLog,
|
|
7
|
+
type LlmJsonPutSchema,
|
|
8
|
+
LlmJsonPut,
|
|
9
|
+
parallel,
|
|
10
|
+
} from "@spaceflow/core";
|
|
11
|
+
import {
|
|
12
|
+
ReviewIssue,
|
|
13
|
+
ReviewSpec,
|
|
14
|
+
ReviewRule,
|
|
15
|
+
ReviewSpecService,
|
|
16
|
+
FileContentsMap,
|
|
17
|
+
FileContentLine,
|
|
18
|
+
} from "./review-spec";
|
|
19
|
+
|
|
20
|
+
interface VerifyResult {
|
|
21
|
+
fixed: boolean;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
reason: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TRUE = "true";
|
|
27
|
+
const FALSE = "false";
|
|
28
|
+
|
|
29
|
+
const VERIFY_SCHEMA: LlmJsonPutSchema = {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
fixed: {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
description: "问题是否已被修复",
|
|
35
|
+
},
|
|
36
|
+
valid: {
|
|
37
|
+
type: "boolean",
|
|
38
|
+
description: "问题是否有效,有效的条件就是你需要看看代码是否符合规范",
|
|
39
|
+
},
|
|
40
|
+
reason: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "判断依据,说明为什么认为问题已修复或仍存在",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["fixed", "valid", "reason"],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
@Injectable()
|
|
50
|
+
export class IssueVerifyService {
|
|
51
|
+
constructor(
|
|
52
|
+
protected readonly llmProxyService: LlmProxyService,
|
|
53
|
+
protected readonly reviewSpecService: ReviewSpecService,
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 验证历史 issues 是否已被修复
|
|
58
|
+
* 按并发数批量验证,每批并行调用 LLM
|
|
59
|
+
*/
|
|
60
|
+
async verifyIssueFixes(
|
|
61
|
+
existingIssues: ReviewIssue[],
|
|
62
|
+
fileContents: FileContentsMap,
|
|
63
|
+
specs: ReviewSpec[],
|
|
64
|
+
llmMode: LLMMode,
|
|
65
|
+
verbose?: VerboseLevel,
|
|
66
|
+
concurrency: number = 10,
|
|
67
|
+
): Promise<ReviewIssue[]> {
|
|
68
|
+
if (existingIssues.length === 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (shouldLog(verbose, 1)) {
|
|
73
|
+
console.log(
|
|
74
|
+
`\n🔍 开始验证 ${existingIssues.length} 个历史问题是否已修复 (并发: ${concurrency})...`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const verifiedIssues: ReviewIssue[] = [];
|
|
79
|
+
const llmJsonPut = new LlmJsonPut<VerifyResult>(VERIFY_SCHEMA);
|
|
80
|
+
|
|
81
|
+
// 预处理:分离已修复和需要验证的 issues
|
|
82
|
+
const toVerify: {
|
|
83
|
+
issue: ReviewIssue;
|
|
84
|
+
fileContent: FileContentLine[];
|
|
85
|
+
ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null;
|
|
86
|
+
}[] = [];
|
|
87
|
+
for (const issue of existingIssues) {
|
|
88
|
+
if (issue.fixed) {
|
|
89
|
+
if (shouldLog(verbose, 1)) {
|
|
90
|
+
console.log(` ⏭️ 跳过已修复: ${issue.file}:${issue.line} (${issue.ruleId})`);
|
|
91
|
+
}
|
|
92
|
+
verifiedIssues.push(issue);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// valid === 'false' 的问题跳过复查(已确认无效的问题无需再次验证)
|
|
97
|
+
if (issue.valid === "false") {
|
|
98
|
+
if (shouldLog(verbose, 1)) {
|
|
99
|
+
console.log(` ⏭️ 跳过无效问题: ${issue.file}:${issue.line} (${issue.ruleId})`);
|
|
100
|
+
}
|
|
101
|
+
verifiedIssues.push(issue);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fileContent = fileContents.get(issue.file);
|
|
106
|
+
if (fileContent === undefined) {
|
|
107
|
+
if (shouldLog(verbose, 1)) {
|
|
108
|
+
console.log(` ✅ 文件已删除: ${issue.file}:${issue.line} (${issue.ruleId})`);
|
|
109
|
+
}
|
|
110
|
+
verifiedIssues.push({
|
|
111
|
+
...issue,
|
|
112
|
+
fixed: new Date().toISOString(),
|
|
113
|
+
valid: FALSE,
|
|
114
|
+
reason: "文件已删除",
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ruleInfo = this.reviewSpecService.findRuleById(issue.ruleId, specs);
|
|
120
|
+
toVerify.push({ issue, fileContent, ruleInfo });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 使用 parallel 库并行处理
|
|
124
|
+
const executor = parallel({
|
|
125
|
+
concurrency,
|
|
126
|
+
onTaskStart: (taskId) => {
|
|
127
|
+
if (shouldLog(verbose, 1)) {
|
|
128
|
+
console.log(` 🔎 验证: ${taskId}`);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
onTaskComplete: (taskId, success) => {
|
|
132
|
+
if (shouldLog(verbose, 1)) {
|
|
133
|
+
console.log(` ${success ? "✅" : "❌"} 完成: ${taskId}`);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const results = await executor.map(
|
|
139
|
+
toVerify,
|
|
140
|
+
async ({ issue, fileContent, ruleInfo }) =>
|
|
141
|
+
this.verifySingleIssue(issue, fileContent, ruleInfo, llmMode, llmJsonPut, verbose),
|
|
142
|
+
({ issue }) => `${issue.file}:${issue.line}`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
for (const result of results) {
|
|
146
|
+
if (result.success && result.result) {
|
|
147
|
+
verifiedIssues.push(result.result);
|
|
148
|
+
} else {
|
|
149
|
+
// 失败时保留原始 issue
|
|
150
|
+
const originalItem = toVerify.find(
|
|
151
|
+
(item) => `${item.issue.file}:${item.issue.line}` === result.id,
|
|
152
|
+
);
|
|
153
|
+
if (originalItem) {
|
|
154
|
+
verifiedIssues.push(originalItem.issue);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fixedCount = verifiedIssues.filter((i) => i.fixed).length;
|
|
160
|
+
const unfixedCount = verifiedIssues.length - fixedCount;
|
|
161
|
+
if (shouldLog(verbose, 1)) {
|
|
162
|
+
console.log(`\n📊 验证完成: ${fixedCount} 个已修复, ${unfixedCount} 个未修复`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return verifiedIssues;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 验证单个 issue 是否已修复
|
|
170
|
+
*/
|
|
171
|
+
protected async verifySingleIssue(
|
|
172
|
+
issue: ReviewIssue,
|
|
173
|
+
fileContent: FileContentLine[],
|
|
174
|
+
ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null,
|
|
175
|
+
llmMode: LLMMode,
|
|
176
|
+
llmJsonPut: LlmJsonPut<VerifyResult>,
|
|
177
|
+
verbose?: VerboseLevel,
|
|
178
|
+
): Promise<ReviewIssue> {
|
|
179
|
+
const verifyPrompt = this.buildVerifyPrompt(issue, fileContent, ruleInfo);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const stream = this.llmProxyService.chatStream(
|
|
183
|
+
[
|
|
184
|
+
{ role: "system", content: verifyPrompt.systemPrompt },
|
|
185
|
+
{ role: "user", content: verifyPrompt.userPrompt },
|
|
186
|
+
],
|
|
187
|
+
{
|
|
188
|
+
adapter: llmMode,
|
|
189
|
+
jsonSchema: llmJsonPut,
|
|
190
|
+
verbose,
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
let result: VerifyResult | undefined;
|
|
195
|
+
for await (const event of stream) {
|
|
196
|
+
if (event.type === "result") {
|
|
197
|
+
result = event.response.structuredOutput as VerifyResult | undefined;
|
|
198
|
+
} else if (event.type === "error") {
|
|
199
|
+
console.error(` ❌ 验证失败: ${event.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (result) {
|
|
204
|
+
const updatedIssue: ReviewIssue = {
|
|
205
|
+
...issue,
|
|
206
|
+
valid: result.valid ? TRUE : FALSE,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (result.fixed) {
|
|
210
|
+
if (shouldLog(verbose, 1)) {
|
|
211
|
+
console.log(` ✅ 已修复: ${result.reason}`);
|
|
212
|
+
}
|
|
213
|
+
updatedIssue.fixed = new Date().toISOString();
|
|
214
|
+
} else if (!result.valid) {
|
|
215
|
+
if (shouldLog(verbose, 1)) {
|
|
216
|
+
console.log(` ❌ 无效问题: ${result.reason}`);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
if (shouldLog(verbose, 1)) {
|
|
220
|
+
console.log(` ⚠️ 未修复: ${result.reason}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return updatedIssue;
|
|
225
|
+
} else {
|
|
226
|
+
return issue;
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error instanceof Error) {
|
|
230
|
+
console.error(` ❌ 验证出错: ${error.message}`);
|
|
231
|
+
if (error.stack) {
|
|
232
|
+
console.error(` 堆栈信息:\n${error.stack}`);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
console.error(` ❌ 验证出错: ${String(error)}`);
|
|
236
|
+
}
|
|
237
|
+
return issue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 构建验证单个 issue 是否已修复的 prompt
|
|
243
|
+
*/
|
|
244
|
+
protected buildVerifyPrompt(
|
|
245
|
+
issue: ReviewIssue,
|
|
246
|
+
fileContent: FileContentLine[],
|
|
247
|
+
ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null,
|
|
248
|
+
): { systemPrompt: string; userPrompt: string } {
|
|
249
|
+
const padWidth = String(fileContent.length).length;
|
|
250
|
+
const linesWithNumbers = fileContent
|
|
251
|
+
.map(([, line], index) => `${String(index + 1).padStart(padWidth)}| ${line}`)
|
|
252
|
+
.join("\n");
|
|
253
|
+
|
|
254
|
+
const systemPrompt = `你是一个代码审查专家。你的任务是判断之前发现的一个代码问题:
|
|
255
|
+
1. 是否有效(是否真的违反了规则)
|
|
256
|
+
2. 是否已经被修复
|
|
257
|
+
|
|
258
|
+
请仔细分析当前的代码内容。
|
|
259
|
+
|
|
260
|
+
## 输出要求
|
|
261
|
+
- valid: 布尔值,true 表示问题有效(代码确实违反了规则),false 表示问题无效(误报)
|
|
262
|
+
- fixed: 布尔值,true 表示问题已经被修复,false 表示问题仍然存在
|
|
263
|
+
- reason: 判断依据
|
|
264
|
+
|
|
265
|
+
## 判断标准
|
|
266
|
+
|
|
267
|
+
### valid 判断
|
|
268
|
+
- 根据规则 ID 和问题描述,判断代码是否真的违反了该规则
|
|
269
|
+
- 如果问题描述与实际代码不符,valid 为 false
|
|
270
|
+
- 如果规则不适用于该代码场景,valid 为 false
|
|
271
|
+
|
|
272
|
+
### fixed 判断
|
|
273
|
+
- 只有当问题所在的代码已被修改,且修改后的代码不再违反规则时,fixed 才为 true
|
|
274
|
+
- 如果问题所在的代码仍然存在且仍违反规则,fixed 必须为 false
|
|
275
|
+
- 如果代码行号发生变化但问题本质仍存在,fixed 必须为 false
|
|
276
|
+
|
|
277
|
+
## 重要提醒
|
|
278
|
+
- valid=false 时,fixed 的值无意义(无效问题无需修复)
|
|
279
|
+
- 请确保 valid 和 fixed 的值与 reason 的描述一致!`;
|
|
280
|
+
|
|
281
|
+
// 构建规则定义部分
|
|
282
|
+
let ruleSection = "";
|
|
283
|
+
if (ruleInfo) {
|
|
284
|
+
ruleSection = this.reviewSpecService.buildSpecsSection([ruleInfo.spec]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const userPrompt = `## 规则定义
|
|
288
|
+
|
|
289
|
+
${ruleSection}
|
|
290
|
+
|
|
291
|
+
## 之前发现的问题
|
|
292
|
+
|
|
293
|
+
- **文件**: ${issue.file}
|
|
294
|
+
- **行号**: ${issue.line}
|
|
295
|
+
- **规则**: ${issue.ruleId} (来自 ${issue.specFile})
|
|
296
|
+
- **问题描述**: ${issue.reason}
|
|
297
|
+
${issue.suggestion ? `- **原建议**: ${issue.suggestion}` : ""}
|
|
298
|
+
|
|
299
|
+
## 当前文件内容
|
|
300
|
+
|
|
301
|
+
\`\`\`
|
|
302
|
+
${linesWithNumbers}
|
|
303
|
+
\`\`\`
|
|
304
|
+
|
|
305
|
+
请判断这个问题是否有效,以及是否已经被修复。`;
|
|
306
|
+
|
|
307
|
+
return { systemPrompt, userPrompt };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Code review command using LLM for automated PR review",
|
|
3
|
+
"options.dryRun": "Only print actions without posting comments",
|
|
4
|
+
"options.prNumber": "PR number, auto-detected from env if not specified",
|
|
5
|
+
"options.base": "Base branch/tag for diff comparison",
|
|
6
|
+
"options.head": "Head branch/tag for diff comparison",
|
|
7
|
+
"options.includes": "File glob patterns to review, e.g. *.ts *.js (can be specified multiple times)",
|
|
8
|
+
"options.llmMode": "LLM mode: claude-code, openai, gemini",
|
|
9
|
+
"options.files": "Only review specified files (space-separated)",
|
|
10
|
+
"options.commits": "Only review specified commits (space-separated)",
|
|
11
|
+
"options.verifyFixes": "Verify if historical issues are fixed (default from config)",
|
|
12
|
+
"options.noVerifyFixes": "Disable historical issue verification",
|
|
13
|
+
"options.verifyConcurrency": "Concurrency for verifying historical issues (default 10)",
|
|
14
|
+
"options.analyzeDeletions": "Analyze impact of deleted code (true, false, ci, pr, terminal)",
|
|
15
|
+
"options.deletionAnalysisMode": "Deletion analysis mode: openai (standard) or claude-code (Agent mode with tools)",
|
|
16
|
+
"options.deletionOnly": "Only run deletion analysis, skip regular code review",
|
|
17
|
+
"options.outputFormat": "Output format: markdown, terminal, json. Auto-selected if not specified (markdown for PR, terminal for CLI)",
|
|
18
|
+
"options.generateDescription": "Generate PR description using AI",
|
|
19
|
+
"options.showAll": "Show all issues found, including those on unchanged lines",
|
|
20
|
+
"options.eventAction": "PR event type (opened, synchronize, closed, etc.), closed only collects stats without AI review",
|
|
21
|
+
"extensionDescription": "Code review command using LLM for automated PR review",
|
|
22
|
+
"mcp.serverDescription": "Code review rules query service",
|
|
23
|
+
"mcp.listRules": "List all code review rules for the current project, returning rule list with ID, title, description, applicable file extensions, etc.",
|
|
24
|
+
"mcp.getRulesForFile": "Get applicable code review rules for a specific file, filtered by file extension and includes configuration",
|
|
25
|
+
"mcp.getRuleDetail": "Get full details of a specific rule, including description, example code, etc.",
|
|
26
|
+
"mcp.ruleNotFound": "Rule {{ruleId}} not found",
|
|
27
|
+
"mcp.dto.cwd": "Project root directory path, defaults to current working directory",
|
|
28
|
+
"mcp.dto.filePath": "File path, can be relative or absolute",
|
|
29
|
+
"mcp.dto.includeExamples": "Whether to include rule example code, defaults to false",
|
|
30
|
+
"mcp.dto.ruleId": "Rule ID, e.g. JsTs.Naming.FileName"
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { addLocaleResources } from "@spaceflow/core";
|
|
2
|
+
import zhCN from "./zh-cn/review.json";
|
|
3
|
+
import en from "./en/review.json";
|
|
4
|
+
|
|
5
|
+
/** review 命令 i18n 资源 */
|
|
6
|
+
export const reviewLocales: Record<string, Record<string, string>> = {
|
|
7
|
+
"zh-CN": zhCN,
|
|
8
|
+
en,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
addLocaleResources("review", reviewLocales);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "代码审查命令,使用 LLM 对 PR 代码进行自动审查",
|
|
3
|
+
"options.dryRun": "仅打印将要执行的操作,不实际提交评论",
|
|
4
|
+
"options.prNumber": "PR 编号,如果不指定则从环境变量获取",
|
|
5
|
+
"options.base": "基准分支/tag,用于比较差异",
|
|
6
|
+
"options.head": "目标分支/tag,用于比较差异",
|
|
7
|
+
"options.includes": "要审查的文件 glob 模式,如 *.ts *.js(可多次指定)",
|
|
8
|
+
"options.llmMode": "使用的 LLM 模式: claude-code, openai, gemini",
|
|
9
|
+
"options.files": "仅审查指定的文件(空格分隔)",
|
|
10
|
+
"options.commits": "仅审查指定的 commits(空格分隔)",
|
|
11
|
+
"options.verifyFixes": "是否验证历史问题是否已修复(默认从配置文件读取)",
|
|
12
|
+
"options.noVerifyFixes": "禁用历史问题验证",
|
|
13
|
+
"options.verifyConcurrency": "验证历史问题的并发数(默认 10)",
|
|
14
|
+
"options.analyzeDeletions": "分析删除代码可能带来的影响 (true, false, ci, pr, terminal)",
|
|
15
|
+
"options.deletionAnalysisMode": "删除代码分析模式: openai (标准模式) 或 claude-code (Agent 模式,可使用工具)",
|
|
16
|
+
"options.deletionOnly": "仅执行删除代码分析,跳过常规代码审查",
|
|
17
|
+
"options.outputFormat": "输出格式: markdown, terminal, json。不指定则智能选择(PR 用 markdown,终端用 terminal)",
|
|
18
|
+
"options.generateDescription": "使用 AI 生成 PR 功能描述",
|
|
19
|
+
"options.showAll": "显示所有发现的问题,不过滤非变更行的问题",
|
|
20
|
+
"options.eventAction": "PR 事件类型(opened, synchronize, closed 等),closed 时仅收集统计不进行 AI 审查",
|
|
21
|
+
"extensionDescription": "代码审查命令,使用 LLM 对 PR 代码进行自动审查",
|
|
22
|
+
"mcp.serverDescription": "代码审查规则查询服务",
|
|
23
|
+
"mcp.listRules": "获取当前项目的所有代码审查规则,返回规则列表包含 ID、标题、描述、适用的文件扩展名等信息",
|
|
24
|
+
"mcp.getRulesForFile": "获取某个文件应该使用的代码审查规则,根据文件扩展名和 includes 配置过滤",
|
|
25
|
+
"mcp.getRuleDetail": "获取某个规则的完整详情,包括描述、示例代码等",
|
|
26
|
+
"mcp.ruleNotFound": "规则 {{ruleId}} 不存在",
|
|
27
|
+
"mcp.dto.cwd": "项目根目录路径,默认为当前工作目录",
|
|
28
|
+
"mcp.dto.filePath": "文件路径,可以是相对路径或绝对路径",
|
|
29
|
+
"mcp.dto.includeExamples": "是否包含规则示例代码,默认 false",
|
|
30
|
+
"mcp.dto.ruleId": "规则 ID,如 JsTs.Naming.FileName"
|
|
31
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { parseTitleOptions } from "./parse-title-options";
|
|
2
|
+
|
|
3
|
+
describe("parseTitleOptions", () => {
|
|
4
|
+
describe("基本解析", () => {
|
|
5
|
+
it("应该从 PR 标题末尾解析命令参数 (/review)", () => {
|
|
6
|
+
const title = "feat: 添加新功能 [/review -l openai -v 2]";
|
|
7
|
+
const options = parseTitleOptions(title);
|
|
8
|
+
|
|
9
|
+
expect(options.llmMode).toBe("openai");
|
|
10
|
+
expect(options.verbose).toBe(2);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("应该支持旧的 /ai-review 格式", () => {
|
|
14
|
+
const title = "feat: 添加新功能 [/ai-review -l openai -v 2]";
|
|
15
|
+
const options = parseTitleOptions(title);
|
|
16
|
+
|
|
17
|
+
expect(options.llmMode).toBe("openai");
|
|
18
|
+
expect(options.verbose).toBe(2);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("没有命令参数时应返回空对象", () => {
|
|
22
|
+
const title = "feat: 添加新功能";
|
|
23
|
+
const options = parseTitleOptions(title);
|
|
24
|
+
|
|
25
|
+
expect(options).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("格式不正确时应返回空对象", () => {
|
|
29
|
+
const title = "feat: 添加新功能 [ai-review -l openai]"; // 缺少 /
|
|
30
|
+
const options = parseTitleOptions(title);
|
|
31
|
+
|
|
32
|
+
expect(options).toEqual({});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("LLM 模式参数", () => {
|
|
37
|
+
it("应该解析 -l 短参数", () => {
|
|
38
|
+
const title = "fix: bug [/ai-review -l claude-code]";
|
|
39
|
+
const options = parseTitleOptions(title);
|
|
40
|
+
|
|
41
|
+
expect(options.llmMode).toBe("claude-code");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("应该解析 --llm-mode 长参数", () => {
|
|
45
|
+
const title = "fix: bug [/ai-review --llm-mode gemini]";
|
|
46
|
+
const options = parseTitleOptions(title);
|
|
47
|
+
|
|
48
|
+
expect(options.llmMode).toBe("gemini");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("无效的 LLM 模式应被忽略", () => {
|
|
52
|
+
const title = "fix: bug [/ai-review -l invalid-mode]";
|
|
53
|
+
const options = parseTitleOptions(title);
|
|
54
|
+
|
|
55
|
+
expect(options.llmMode).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("详细输出级别参数", () => {
|
|
60
|
+
it("应该解析 -v 1", () => {
|
|
61
|
+
const title = "fix: bug [/ai-review -v 1]";
|
|
62
|
+
const options = parseTitleOptions(title);
|
|
63
|
+
|
|
64
|
+
expect(options.verbose).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("应该解析 -v 2", () => {
|
|
68
|
+
const title = "fix: bug [/ai-review -v 2]";
|
|
69
|
+
const options = parseTitleOptions(title);
|
|
70
|
+
|
|
71
|
+
expect(options.verbose).toBe(2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("-v 不带值时应默认为 1", () => {
|
|
75
|
+
const title = "fix: bug [/ai-review -v]";
|
|
76
|
+
const options = parseTitleOptions(title);
|
|
77
|
+
|
|
78
|
+
expect(options.verbose).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("-v 后跟其他参数时应默认为 1", () => {
|
|
82
|
+
const title = "fix: bug [/ai-review -v -l openai]";
|
|
83
|
+
const options = parseTitleOptions(title);
|
|
84
|
+
|
|
85
|
+
expect(options.verbose).toBe(1);
|
|
86
|
+
expect(options.llmMode).toBe("openai");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("应该解析 --verbose 长参数", () => {
|
|
90
|
+
const title = "fix: bug [/ai-review --verbose 2]";
|
|
91
|
+
const options = parseTitleOptions(title);
|
|
92
|
+
|
|
93
|
+
expect(options.verbose).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("dry-run 参数", () => {
|
|
98
|
+
it("应该解析 -d 短参数", () => {
|
|
99
|
+
const title = "fix: bug [/ai-review -d]";
|
|
100
|
+
const options = parseTitleOptions(title);
|
|
101
|
+
|
|
102
|
+
expect(options.dryRun).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("应该解析 --dry-run 长参数", () => {
|
|
106
|
+
const title = "fix: bug [/ai-review --dry-run]";
|
|
107
|
+
const options = parseTitleOptions(title);
|
|
108
|
+
|
|
109
|
+
expect(options.dryRun).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("includes 参数", () => {
|
|
114
|
+
it("应该解析 -i 短参数", () => {
|
|
115
|
+
const title = "fix: bug [/ai-review -i *.ts]";
|
|
116
|
+
const options = parseTitleOptions(title);
|
|
117
|
+
|
|
118
|
+
expect(options.includes).toEqual(["*.ts"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("应该解析多个 includes", () => {
|
|
122
|
+
const title = "fix: bug [/ai-review -i *.ts -i *.js]";
|
|
123
|
+
const options = parseTitleOptions(title);
|
|
124
|
+
|
|
125
|
+
expect(options.includes).toEqual(["*.ts", "*.js"]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("verify-fixes 参数", () => {
|
|
130
|
+
it("应该解析 --verify-fixes", () => {
|
|
131
|
+
const title = "fix: bug [/ai-review --verify-fixes]";
|
|
132
|
+
const options = parseTitleOptions(title);
|
|
133
|
+
|
|
134
|
+
expect(options.verifyFixes).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("应该解析 --no-verify-fixes", () => {
|
|
138
|
+
const title = "fix: bug [/ai-review --no-verify-fixes]";
|
|
139
|
+
const options = parseTitleOptions(title);
|
|
140
|
+
|
|
141
|
+
expect(options.verifyFixes).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("删除代码分析参数", () => {
|
|
146
|
+
it("应该解析 --analyze-deletions 无值时默认为 true", () => {
|
|
147
|
+
const title = "fix: bug [/ai-review --analyze-deletions]";
|
|
148
|
+
const options = parseTitleOptions(title);
|
|
149
|
+
|
|
150
|
+
expect(options.analyzeDeletions).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("应该解析 --analyze-deletions true", () => {
|
|
154
|
+
const title = "fix: bug [/ai-review --analyze-deletions true]";
|
|
155
|
+
const options = parseTitleOptions(title);
|
|
156
|
+
|
|
157
|
+
expect(options.analyzeDeletions).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("应该解析 --analyze-deletions false", () => {
|
|
161
|
+
const title = "fix: bug [/ai-review --analyze-deletions false]";
|
|
162
|
+
const options = parseTitleOptions(title);
|
|
163
|
+
|
|
164
|
+
expect(options.analyzeDeletions).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("应该解析 --analyze-deletions ci", () => {
|
|
168
|
+
const title = "fix: bug [/ai-review --analyze-deletions ci]";
|
|
169
|
+
const options = parseTitleOptions(title);
|
|
170
|
+
|
|
171
|
+
expect(options.analyzeDeletions).toBe("ci");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("应该解析 --analyze-deletions pr", () => {
|
|
175
|
+
const title = "fix: bug [/ai-review --analyze-deletions pr]";
|
|
176
|
+
const options = parseTitleOptions(title);
|
|
177
|
+
|
|
178
|
+
expect(options.analyzeDeletions).toBe("pr");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("应该解析 --analyze-deletions terminal", () => {
|
|
182
|
+
const title = "fix: bug [/ai-review --analyze-deletions terminal]";
|
|
183
|
+
const options = parseTitleOptions(title);
|
|
184
|
+
|
|
185
|
+
expect(options.analyzeDeletions).toBe("terminal");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("应该解析 --deletion-only", () => {
|
|
189
|
+
const title = "fix: bug [/ai-review --deletion-only]";
|
|
190
|
+
const options = parseTitleOptions(title);
|
|
191
|
+
|
|
192
|
+
expect(options.deletionOnly).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("应该解析 --deletion-analysis-mode", () => {
|
|
196
|
+
const title = "fix: bug [/ai-review --deletion-analysis-mode claude-code]";
|
|
197
|
+
const options = parseTitleOptions(title);
|
|
198
|
+
|
|
199
|
+
expect(options.deletionAnalysisMode).toBe("claude-code");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("组合参数", () => {
|
|
204
|
+
it("应该正确解析多个参数组合", () => {
|
|
205
|
+
const title = "feat: 新功能 [/ai-review -l openai -v 2 -d --no-verify-fixes]";
|
|
206
|
+
const options = parseTitleOptions(title);
|
|
207
|
+
|
|
208
|
+
expect(options.llmMode).toBe("openai");
|
|
209
|
+
expect(options.verbose).toBe(2);
|
|
210
|
+
expect(options.dryRun).toBe(true);
|
|
211
|
+
expect(options.verifyFixes).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("应该处理带引号的参数值", () => {
|
|
215
|
+
const title = 'fix: bug [/ai-review -i "src/**/*.ts"]';
|
|
216
|
+
const options = parseTitleOptions(title);
|
|
217
|
+
|
|
218
|
+
expect(options.includes).toEqual(["src/**/*.ts"]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("大小写不敏感", () => {
|
|
223
|
+
it("命令名称应该大小写不敏感", () => {
|
|
224
|
+
const title = "fix: bug [/AI-REVIEW -l openai]";
|
|
225
|
+
const options = parseTitleOptions(title);
|
|
226
|
+
|
|
227
|
+
expect(options.llmMode).toBe("openai");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("边界情况", () => {
|
|
232
|
+
it("空标题应返回空对象", () => {
|
|
233
|
+
const options = parseTitleOptions("");
|
|
234
|
+
expect(options).toEqual({});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("命令在标题中间也应该被解析", () => {
|
|
238
|
+
const title = "feat: [/review -l openai] 替换 [/ai-review -l openai] 添加新功能";
|
|
239
|
+
const options = parseTitleOptions(title);
|
|
240
|
+
|
|
241
|
+
expect(options.llmMode).toBe("openai");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("多个命令只解析第一个", () => {
|
|
245
|
+
const title = "feat: [/review -l openai] 替换 [/ai-review -l openai] [/ai-review -l gemini]";
|
|
246
|
+
const options = parseTitleOptions(title);
|
|
247
|
+
|
|
248
|
+
expect(options.llmMode).toBe("openai");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|