@spaceflow/review 0.76.0 → 0.78.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 +47 -0
- package/dist/index.js +3830 -2469
- package/package.json +2 -2
- package/src/deletion-impact.service.ts +17 -130
- package/src/index.ts +34 -2
- package/src/issue-verify.service.ts +18 -82
- package/src/locales/en/review.json +2 -1
- package/src/locales/zh-cn/review.json +2 -1
- package/src/mcp/index.ts +4 -1
- package/src/prompt/code-review.ts +95 -0
- package/src/prompt/deletion-impact.ts +105 -0
- package/src/prompt/index.ts +37 -0
- package/src/prompt/issue-verify.ts +86 -0
- package/src/prompt/pr-description.ts +149 -0
- package/src/prompt/schemas.ts +106 -0
- package/src/prompt/types.ts +53 -0
- package/src/pull-request-model.ts +236 -0
- package/src/review-context.ts +433 -0
- package/src/review-includes-filter.spec.ts +284 -0
- package/src/review-includes-filter.ts +196 -0
- package/src/review-issue-filter.ts +523 -0
- package/src/review-llm.ts +543 -0
- package/src/review-result-model.spec.ts +657 -0
- package/src/review-result-model.ts +1046 -0
- package/src/review-spec/review-spec.service.ts +26 -5
- package/src/review-spec/types.ts +2 -0
- package/src/review.config.ts +40 -5
- package/src/review.service.spec.ts +102 -1625
- package/src/review.service.ts +608 -2742
- package/src/system-rules/index.ts +48 -0
- package/src/system-rules/max-lines-per-file.ts +57 -0
- package/src/types/review-llm.ts +21 -0
- package/src/utils/review-llm.spec.ts +277 -0
- package/src/utils/review-llm.ts +177 -0
- package/src/utils/review-pr-comment.spec.ts +340 -0
- package/src/utils/review-pr-comment.ts +186 -0
- package/tsconfig.json +1 -1
|
@@ -3,6 +3,8 @@ import { parseChangedLinesFromPatch } from "@spaceflow/core";
|
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
4
|
import { ReviewService, ReviewContext, ReviewPrompt } from "./review.service";
|
|
5
5
|
import type { ReviewOptions } from "./review.config";
|
|
6
|
+
import { PullRequestModel } from "./pull-request-model";
|
|
7
|
+
import { ReviewResultModel } from "./review-result-model";
|
|
6
8
|
|
|
7
9
|
vi.mock("c12");
|
|
8
10
|
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
@@ -215,9 +217,8 @@ describe("ReviewService", () => {
|
|
|
215
217
|
issues: [],
|
|
216
218
|
summary: [],
|
|
217
219
|
});
|
|
218
|
-
vi.spyOn(service as any, "buildLineCommitMap").mockResolvedValue(new Map());
|
|
219
220
|
vi.spyOn(service as any, "getFileContents").mockResolvedValue(new Map());
|
|
220
|
-
vi.spyOn(
|
|
221
|
+
vi.spyOn(ReviewResultModel, "loadFromPr").mockResolvedValue(null as any);
|
|
221
222
|
});
|
|
222
223
|
|
|
223
224
|
it("should execute review for PR successfully", async () => {
|
|
@@ -563,7 +564,7 @@ describe("ReviewService", () => {
|
|
|
563
564
|
describe("ReviewService.runLLMReview", () => {
|
|
564
565
|
it("should call callLLM when llmMode is claude", async () => {
|
|
565
566
|
const callLLMSpy = vi
|
|
566
|
-
.spyOn(service as any, "callLLM")
|
|
567
|
+
.spyOn((service as any).llmProcessor, "callLLM")
|
|
567
568
|
.mockResolvedValue({ issues: [], summary: [] });
|
|
568
569
|
|
|
569
570
|
const mockPrompt: ReviewPrompt = {
|
|
@@ -577,7 +578,7 @@ describe("ReviewService", () => {
|
|
|
577
578
|
|
|
578
579
|
it("should call callLLM when llmMode is openai", async () => {
|
|
579
580
|
const callLLMSpy = vi
|
|
580
|
-
.spyOn(service as any, "callLLM")
|
|
581
|
+
.spyOn((service as any).llmProcessor, "callLLM")
|
|
581
582
|
.mockResolvedValue({ issues: [], summary: [] });
|
|
582
583
|
|
|
583
584
|
const mockPrompt: ReviewPrompt = {
|
|
@@ -602,7 +603,7 @@ describe("ReviewService", () => {
|
|
|
602
603
|
suggestion: "fix",
|
|
603
604
|
} as any,
|
|
604
605
|
];
|
|
605
|
-
const normalized = (service as any).normalizeIssues(issues);
|
|
606
|
+
const normalized = (service as any).llmProcessor.normalizeIssues(issues);
|
|
606
607
|
expect(normalized).toHaveLength(2);
|
|
607
608
|
expect(normalized[0].line).toBe("10");
|
|
608
609
|
expect(normalized[1].line).toBe("12");
|
|
@@ -621,269 +622,6 @@ describe("ReviewService", () => {
|
|
|
621
622
|
});
|
|
622
623
|
});
|
|
623
624
|
|
|
624
|
-
describe("ReviewService.updateIssueLineNumbers", () => {
|
|
625
|
-
beforeEach(() => {
|
|
626
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockImplementation((lineStr: string) => {
|
|
627
|
-
const lines: number[] = [];
|
|
628
|
-
const rangeMatch = lineStr.match(/^(\d+)-(\d+)$/);
|
|
629
|
-
if (rangeMatch) {
|
|
630
|
-
const start = parseInt(rangeMatch[1], 10);
|
|
631
|
-
const end = parseInt(rangeMatch[2], 10);
|
|
632
|
-
for (let i = start; i <= end; i++) {
|
|
633
|
-
lines.push(i);
|
|
634
|
-
}
|
|
635
|
-
} else {
|
|
636
|
-
const line = parseInt(lineStr, 10);
|
|
637
|
-
if (!isNaN(line)) {
|
|
638
|
-
lines.push(line);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return lines;
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
it("should update issue line numbers when code is inserted before", () => {
|
|
646
|
-
// 在第1行插入2行,原第5行变成第7行
|
|
647
|
-
const issues = [
|
|
648
|
-
{
|
|
649
|
-
file: "test.ts",
|
|
650
|
-
line: "5",
|
|
651
|
-
ruleId: "R1",
|
|
652
|
-
specFile: "s1.md",
|
|
653
|
-
reason: "test issue",
|
|
654
|
-
severity: "error",
|
|
655
|
-
code: "",
|
|
656
|
-
round: 1,
|
|
657
|
-
} as any,
|
|
658
|
-
];
|
|
659
|
-
const filePatchMap = new Map<string, string>([
|
|
660
|
-
[
|
|
661
|
-
"test.ts",
|
|
662
|
-
`@@ -1,3 +1,5 @@
|
|
663
|
-
line1
|
|
664
|
-
+new line 1
|
|
665
|
-
+new line 2
|
|
666
|
-
line2
|
|
667
|
-
line3`,
|
|
668
|
-
],
|
|
669
|
-
]);
|
|
670
|
-
|
|
671
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
672
|
-
|
|
673
|
-
expect(result[0].line).toBe("7");
|
|
674
|
-
expect(result[0].originalLine).toBe("5");
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
it("should update issue line numbers when code is deleted before", () => {
|
|
678
|
-
// 删除第1-2行,原第5行变成第3行
|
|
679
|
-
const issues = [
|
|
680
|
-
{
|
|
681
|
-
file: "test.ts",
|
|
682
|
-
line: "5",
|
|
683
|
-
ruleId: "R1",
|
|
684
|
-
specFile: "s1.md",
|
|
685
|
-
reason: "test issue",
|
|
686
|
-
severity: "error",
|
|
687
|
-
code: "",
|
|
688
|
-
round: 1,
|
|
689
|
-
} as any,
|
|
690
|
-
];
|
|
691
|
-
const filePatchMap = new Map<string, string>([
|
|
692
|
-
[
|
|
693
|
-
"test.ts",
|
|
694
|
-
`@@ -1,4 +1,2 @@
|
|
695
|
-
-line1
|
|
696
|
-
-line2
|
|
697
|
-
line3
|
|
698
|
-
line4`,
|
|
699
|
-
],
|
|
700
|
-
]);
|
|
701
|
-
|
|
702
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
703
|
-
|
|
704
|
-
expect(result[0].line).toBe("3");
|
|
705
|
-
expect(result[0].originalLine).toBe("5");
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
it("should mark issue as invalid when the line is deleted", () => {
|
|
709
|
-
// 删除第5行
|
|
710
|
-
const issues = [
|
|
711
|
-
{
|
|
712
|
-
file: "test.ts",
|
|
713
|
-
line: "5",
|
|
714
|
-
ruleId: "R1",
|
|
715
|
-
specFile: "s1.md",
|
|
716
|
-
reason: "test issue",
|
|
717
|
-
severity: "error",
|
|
718
|
-
code: "",
|
|
719
|
-
round: 1,
|
|
720
|
-
} as any,
|
|
721
|
-
];
|
|
722
|
-
const filePatchMap = new Map<string, string>([
|
|
723
|
-
[
|
|
724
|
-
"test.ts",
|
|
725
|
-
`@@ -5,1 +5,0 @@
|
|
726
|
-
-deleted line`,
|
|
727
|
-
],
|
|
728
|
-
]);
|
|
729
|
-
|
|
730
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
731
|
-
|
|
732
|
-
expect(result[0].valid).toBe("false");
|
|
733
|
-
expect(result[0].originalLine).toBe("5");
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
it("should not update issue when file has no changes", () => {
|
|
737
|
-
const issues = [
|
|
738
|
-
{
|
|
739
|
-
file: "test.ts",
|
|
740
|
-
line: "5",
|
|
741
|
-
ruleId: "R1",
|
|
742
|
-
specFile: "s1.md",
|
|
743
|
-
reason: "test issue",
|
|
744
|
-
severity: "error",
|
|
745
|
-
code: "",
|
|
746
|
-
round: 1,
|
|
747
|
-
} as any,
|
|
748
|
-
];
|
|
749
|
-
const filePatchMap = new Map<string, string>([
|
|
750
|
-
[
|
|
751
|
-
"other.ts",
|
|
752
|
-
`@@ -1,1 +1,2 @@
|
|
753
|
-
line1
|
|
754
|
-
+new line`,
|
|
755
|
-
],
|
|
756
|
-
]);
|
|
757
|
-
|
|
758
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
759
|
-
|
|
760
|
-
expect(result[0].line).toBe("5");
|
|
761
|
-
expect(result[0].originalLine).toBeUndefined();
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
it("should not update already fixed issues", () => {
|
|
765
|
-
const issues = [
|
|
766
|
-
{
|
|
767
|
-
file: "test.ts",
|
|
768
|
-
line: "5",
|
|
769
|
-
ruleId: "R1",
|
|
770
|
-
specFile: "s1.md",
|
|
771
|
-
reason: "test issue",
|
|
772
|
-
severity: "error",
|
|
773
|
-
code: "",
|
|
774
|
-
round: 1,
|
|
775
|
-
fixed: "2024-01-01T00:00:00Z",
|
|
776
|
-
} as any,
|
|
777
|
-
];
|
|
778
|
-
const filePatchMap = new Map<string, string>([
|
|
779
|
-
[
|
|
780
|
-
"test.ts",
|
|
781
|
-
`@@ -1,1 +1,3 @@
|
|
782
|
-
line1
|
|
783
|
-
+new line 1
|
|
784
|
-
+new line 2`,
|
|
785
|
-
],
|
|
786
|
-
]);
|
|
787
|
-
|
|
788
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
789
|
-
|
|
790
|
-
expect(result[0].line).toBe("5");
|
|
791
|
-
expect(result[0].originalLine).toBeUndefined();
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
it("should not update invalid issues", () => {
|
|
795
|
-
const issues = [
|
|
796
|
-
{
|
|
797
|
-
file: "test.ts",
|
|
798
|
-
line: "5",
|
|
799
|
-
ruleId: "R1",
|
|
800
|
-
specFile: "s1.md",
|
|
801
|
-
reason: "test issue",
|
|
802
|
-
severity: "error",
|
|
803
|
-
code: "",
|
|
804
|
-
round: 1,
|
|
805
|
-
valid: "false",
|
|
806
|
-
} as any,
|
|
807
|
-
];
|
|
808
|
-
const filePatchMap = new Map<string, string>([
|
|
809
|
-
[
|
|
810
|
-
"test.ts",
|
|
811
|
-
`@@ -1,1 +1,3 @@
|
|
812
|
-
line1
|
|
813
|
-
+new line 1
|
|
814
|
-
+new line 2`,
|
|
815
|
-
],
|
|
816
|
-
]);
|
|
817
|
-
|
|
818
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
819
|
-
|
|
820
|
-
expect(result[0].line).toBe("5");
|
|
821
|
-
expect(result[0].originalLine).toBeUndefined();
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
it("should handle range line numbers", () => {
|
|
825
|
-
// 在第1行插入2行,原第5-7行变成第7-9行
|
|
826
|
-
const issues = [
|
|
827
|
-
{
|
|
828
|
-
file: "test.ts",
|
|
829
|
-
line: "5-7",
|
|
830
|
-
ruleId: "R1",
|
|
831
|
-
specFile: "s1.md",
|
|
832
|
-
reason: "test issue",
|
|
833
|
-
severity: "error",
|
|
834
|
-
code: "",
|
|
835
|
-
round: 1,
|
|
836
|
-
} as any,
|
|
837
|
-
];
|
|
838
|
-
const filePatchMap = new Map<string, string>([
|
|
839
|
-
[
|
|
840
|
-
"test.ts",
|
|
841
|
-
`@@ -1,3 +1,5 @@
|
|
842
|
-
line1
|
|
843
|
-
+new line 1
|
|
844
|
-
+new line 2
|
|
845
|
-
line2
|
|
846
|
-
line3`,
|
|
847
|
-
],
|
|
848
|
-
]);
|
|
849
|
-
|
|
850
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
851
|
-
|
|
852
|
-
expect(result[0].line).toBe("7-9");
|
|
853
|
-
expect(result[0].originalLine).toBe("5-7");
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
it("should preserve originalLine if already set", () => {
|
|
857
|
-
const issues = [
|
|
858
|
-
{
|
|
859
|
-
file: "test.ts",
|
|
860
|
-
line: "7",
|
|
861
|
-
originalLine: "3",
|
|
862
|
-
ruleId: "R1",
|
|
863
|
-
specFile: "s1.md",
|
|
864
|
-
reason: "test issue",
|
|
865
|
-
severity: "error",
|
|
866
|
-
code: "",
|
|
867
|
-
round: 1,
|
|
868
|
-
} as any,
|
|
869
|
-
];
|
|
870
|
-
const filePatchMap = new Map<string, string>([
|
|
871
|
-
[
|
|
872
|
-
"test.ts",
|
|
873
|
-
`@@ -1,1 +1,3 @@
|
|
874
|
-
line1
|
|
875
|
-
+new line 1
|
|
876
|
-
+new line 2`,
|
|
877
|
-
],
|
|
878
|
-
]);
|
|
879
|
-
|
|
880
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
881
|
-
|
|
882
|
-
expect(result[0].line).toBe("9");
|
|
883
|
-
expect(result[0].originalLine).toBe("3");
|
|
884
|
-
});
|
|
885
|
-
});
|
|
886
|
-
|
|
887
625
|
describe("ReviewService.getFileContents", () => {
|
|
888
626
|
beforeEach(() => {
|
|
889
627
|
mockReviewSpecService.parseLineRange = vi.fn().mockImplementation((lineStr: string) => {
|
|
@@ -1237,206 +975,6 @@ describe("ReviewService", () => {
|
|
|
1237
975
|
});
|
|
1238
976
|
});
|
|
1239
977
|
|
|
1240
|
-
describe("ReviewService.calculateIssueStats", () => {
|
|
1241
|
-
it("should calculate stats for empty array", () => {
|
|
1242
|
-
const stats = (service as any).calculateIssueStats([]);
|
|
1243
|
-
expect(stats).toEqual({
|
|
1244
|
-
total: 0,
|
|
1245
|
-
fixed: 0,
|
|
1246
|
-
resolved: 0,
|
|
1247
|
-
invalid: 0,
|
|
1248
|
-
pending: 0,
|
|
1249
|
-
fixRate: 0,
|
|
1250
|
-
resolveRate: 0,
|
|
1251
|
-
});
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
it("should calculate stats correctly", () => {
|
|
1255
|
-
const issues = [
|
|
1256
|
-
{ fixed: "2024-01-01" },
|
|
1257
|
-
{ fixed: "2024-01-02" },
|
|
1258
|
-
{ resolved: "2024-01-03" },
|
|
1259
|
-
{ valid: "false" },
|
|
1260
|
-
{},
|
|
1261
|
-
{},
|
|
1262
|
-
];
|
|
1263
|
-
const stats = (service as any).calculateIssueStats(issues);
|
|
1264
|
-
expect(stats.total).toBe(6);
|
|
1265
|
-
expect(stats.fixed).toBe(2);
|
|
1266
|
-
expect(stats.resolved).toBe(1);
|
|
1267
|
-
expect(stats.invalid).toBe(1);
|
|
1268
|
-
expect(stats.pending).toBe(2);
|
|
1269
|
-
expect(stats.fixRate).toBe(33.3);
|
|
1270
|
-
});
|
|
1271
|
-
});
|
|
1272
|
-
|
|
1273
|
-
describe("ReviewService.filterSpecsForFile", () => {
|
|
1274
|
-
it("should return empty for files without extension", () => {
|
|
1275
|
-
const specs = [{ extensions: ["ts"], includes: [], rules: [] }];
|
|
1276
|
-
expect((service as any).filterSpecsForFile(specs, "Makefile")).toEqual([]);
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
it("should filter by extension", () => {
|
|
1280
|
-
const specs = [
|
|
1281
|
-
{ extensions: ["ts"], includes: [], rules: [{ id: "R1" }] },
|
|
1282
|
-
{ extensions: ["py"], includes: [], rules: [{ id: "R2" }] },
|
|
1283
|
-
];
|
|
1284
|
-
const result = (service as any).filterSpecsForFile(specs, "src/app.ts");
|
|
1285
|
-
expect(result).toHaveLength(1);
|
|
1286
|
-
expect(result[0].rules[0].id).toBe("R1");
|
|
1287
|
-
});
|
|
1288
|
-
|
|
1289
|
-
it("should filter by includes pattern when present", () => {
|
|
1290
|
-
const specs = [{ extensions: ["ts"], includes: ["**/*.spec.ts"], rules: [{ id: "R1" }] }];
|
|
1291
|
-
expect((service as any).filterSpecsForFile(specs, "src/app.spec.ts")).toHaveLength(1);
|
|
1292
|
-
expect((service as any).filterSpecsForFile(specs, "src/app.ts")).toHaveLength(0);
|
|
1293
|
-
});
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
describe("ReviewService.buildSystemPrompt", () => {
|
|
1297
|
-
it("should include specs section in prompt", () => {
|
|
1298
|
-
const result = (service as any).buildSystemPrompt("## 规则内容");
|
|
1299
|
-
expect(result).toContain("## 规则内容");
|
|
1300
|
-
expect(result).toContain("代码审查专家");
|
|
1301
|
-
});
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
describe("ReviewService.formatReviewComment", () => {
|
|
1305
|
-
it("should use markdown format in CI with PR", () => {
|
|
1306
|
-
const result = { issues: [], summary: [] };
|
|
1307
|
-
(service as any).formatReviewComment(result, { ci: true, prNumber: 1 });
|
|
1308
|
-
expect((service as any).reviewReportService.formatMarkdown).toHaveBeenCalled();
|
|
1309
|
-
});
|
|
1310
|
-
|
|
1311
|
-
it("should use terminal format by default", () => {
|
|
1312
|
-
const result = { issues: [], summary: [] };
|
|
1313
|
-
(service as any).formatReviewComment(result, {});
|
|
1314
|
-
expect((service as any).reviewReportService.format).toHaveBeenCalledWith(result, "terminal");
|
|
1315
|
-
});
|
|
1316
|
-
|
|
1317
|
-
it("should use specified outputFormat", () => {
|
|
1318
|
-
const result = { issues: [], summary: [] };
|
|
1319
|
-
(service as any).formatReviewComment(result, { outputFormat: "markdown" });
|
|
1320
|
-
expect((service as any).reviewReportService.formatMarkdown).toHaveBeenCalled();
|
|
1321
|
-
});
|
|
1322
|
-
});
|
|
1323
|
-
|
|
1324
|
-
describe("ReviewService.lineMatchesPosition", () => {
|
|
1325
|
-
it("should return false when no position", () => {
|
|
1326
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
1327
|
-
expect((service as any).lineMatchesPosition("10", undefined)).toBe(false);
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
it("should return true when position is within range", () => {
|
|
1331
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10, 11, 12]);
|
|
1332
|
-
expect((service as any).lineMatchesPosition("10-12", 11)).toBe(true);
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
it("should return false when position is outside range", () => {
|
|
1336
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10, 11, 12]);
|
|
1337
|
-
expect((service as any).lineMatchesPosition("10-12", 15)).toBe(false);
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
it("should return false for empty line range", () => {
|
|
1341
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([]);
|
|
1342
|
-
expect((service as any).lineMatchesPosition("", 10)).toBe(false);
|
|
1343
|
-
});
|
|
1344
|
-
});
|
|
1345
|
-
|
|
1346
|
-
describe("ReviewService.issueToReviewComment", () => {
|
|
1347
|
-
it("should return null for invalid line", () => {
|
|
1348
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([]);
|
|
1349
|
-
const issue = { file: "test.ts", line: "abc", ruleId: "R1", specFile: "s1.md", reason: "r" };
|
|
1350
|
-
expect((service as any).issueToReviewComment(issue)).toBeNull();
|
|
1351
|
-
});
|
|
1352
|
-
|
|
1353
|
-
it("should convert issue to review comment", () => {
|
|
1354
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
1355
|
-
const issue = {
|
|
1356
|
-
file: "test.ts",
|
|
1357
|
-
line: "10",
|
|
1358
|
-
ruleId: "R1",
|
|
1359
|
-
specFile: "s1.md",
|
|
1360
|
-
reason: "问题描述",
|
|
1361
|
-
severity: "error",
|
|
1362
|
-
author: { login: "dev1" },
|
|
1363
|
-
suggestion: "fix code",
|
|
1364
|
-
};
|
|
1365
|
-
const result = (service as any).issueToReviewComment(issue);
|
|
1366
|
-
expect(result).not.toBeNull();
|
|
1367
|
-
expect(result.path).toBe("test.ts");
|
|
1368
|
-
expect(result.new_position).toBe(10);
|
|
1369
|
-
expect(result.body).toContain("🔴");
|
|
1370
|
-
expect(result.body).toContain("@dev1");
|
|
1371
|
-
expect(result.body).toContain("fix code");
|
|
1372
|
-
});
|
|
1373
|
-
|
|
1374
|
-
it("should handle warn severity", () => {
|
|
1375
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([5]);
|
|
1376
|
-
const issue = {
|
|
1377
|
-
file: "test.ts",
|
|
1378
|
-
line: "5",
|
|
1379
|
-
ruleId: "R1",
|
|
1380
|
-
specFile: "s1.md",
|
|
1381
|
-
reason: "r",
|
|
1382
|
-
severity: "warn",
|
|
1383
|
-
};
|
|
1384
|
-
const result = (service as any).issueToReviewComment(issue);
|
|
1385
|
-
expect(result.body).toContain("🟡");
|
|
1386
|
-
});
|
|
1387
|
-
|
|
1388
|
-
it("should handle issue without author", () => {
|
|
1389
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([5]);
|
|
1390
|
-
const issue = {
|
|
1391
|
-
file: "test.ts",
|
|
1392
|
-
line: "5",
|
|
1393
|
-
ruleId: "R1",
|
|
1394
|
-
specFile: "s1.md",
|
|
1395
|
-
reason: "r",
|
|
1396
|
-
severity: "info",
|
|
1397
|
-
};
|
|
1398
|
-
const result = (service as any).issueToReviewComment(issue);
|
|
1399
|
-
expect(result.body).toContain("未知");
|
|
1400
|
-
expect(result.body).toContain("⚪");
|
|
1401
|
-
});
|
|
1402
|
-
|
|
1403
|
-
it("should include commit info when present", () => {
|
|
1404
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([5]);
|
|
1405
|
-
const issue = {
|
|
1406
|
-
file: "test.ts",
|
|
1407
|
-
line: "5",
|
|
1408
|
-
ruleId: "R1",
|
|
1409
|
-
specFile: "s1.md",
|
|
1410
|
-
reason: "r",
|
|
1411
|
-
commit: "abc1234",
|
|
1412
|
-
};
|
|
1413
|
-
const result = (service as any).issueToReviewComment(issue);
|
|
1414
|
-
expect(result.body).toContain("abc1234");
|
|
1415
|
-
});
|
|
1416
|
-
});
|
|
1417
|
-
|
|
1418
|
-
describe("ReviewService.generateIssueKey", () => {
|
|
1419
|
-
it("should generate key from file, line, and ruleId", () => {
|
|
1420
|
-
const issue = { file: "test.ts", line: "10", ruleId: "R1" };
|
|
1421
|
-
expect((service as any).generateIssueKey(issue)).toBe("test.ts:10:R1");
|
|
1422
|
-
});
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
describe("ReviewService.parseExistingReviewResult", () => {
|
|
1426
|
-
it("should return null when parseMarkdown returns null", () => {
|
|
1427
|
-
const mockReviewReportService = (service as any).reviewReportService;
|
|
1428
|
-
mockReviewReportService.parseMarkdown.mockReturnValue(null);
|
|
1429
|
-
expect((service as any).parseExistingReviewResult("body")).toBeNull();
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
it("should return result from parsed markdown", () => {
|
|
1433
|
-
const mockReviewReportService = (service as any).reviewReportService;
|
|
1434
|
-
const mockResult = { issues: [{ id: 1 }], summary: [] };
|
|
1435
|
-
mockReviewReportService.parseMarkdown.mockReturnValue({ result: mockResult });
|
|
1436
|
-
expect((service as any).parseExistingReviewResult("body")).toEqual(mockResult);
|
|
1437
|
-
});
|
|
1438
|
-
});
|
|
1439
|
-
|
|
1440
978
|
describe("ReviewService.filterDuplicateIssues", () => {
|
|
1441
979
|
it("should filter issues that exist in valid existing issues", () => {
|
|
1442
980
|
const newIssues = [
|
|
@@ -1459,22 +997,6 @@ describe("ReviewService", () => {
|
|
|
1459
997
|
});
|
|
1460
998
|
});
|
|
1461
999
|
|
|
1462
|
-
describe("ReviewService.getFallbackTitle", () => {
|
|
1463
|
-
it("should return first commit message", () => {
|
|
1464
|
-
const commits = [{ commit: { message: "feat: add feature\n\ndetails" } }];
|
|
1465
|
-
expect((service as any).getFallbackTitle(commits)).toBe("feat: add feature");
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
it("should return default when no commits", () => {
|
|
1469
|
-
expect((service as any).getFallbackTitle([])).toBe("PR 更新");
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
it("should truncate long titles", () => {
|
|
1473
|
-
const commits = [{ commit: { message: "a".repeat(100) } }];
|
|
1474
|
-
expect((service as any).getFallbackTitle(commits).length).toBeLessThanOrEqual(50);
|
|
1475
|
-
});
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
1000
|
describe("ReviewService.normalizeFilePaths", () => {
|
|
1479
1001
|
it("should return undefined for empty array", () => {
|
|
1480
1002
|
expect((service as any).normalizeFilePaths([])).toEqual([]);
|
|
@@ -1490,151 +1012,27 @@ describe("ReviewService", () => {
|
|
|
1490
1012
|
});
|
|
1491
1013
|
});
|
|
1492
1014
|
|
|
1493
|
-
describe("ReviewService.
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
gitProvider.listIssueComments.mockRejectedValue(new Error("API error"));
|
|
1513
|
-
const result = await (service as any).getExistingReviewResult("o", "r", 1);
|
|
1514
|
-
expect(result).toBeNull();
|
|
1515
|
-
});
|
|
1516
|
-
});
|
|
1517
|
-
|
|
1518
|
-
describe("ReviewService.deleteExistingAiReviews", () => {
|
|
1519
|
-
it("should delete AI reviews via review API", async () => {
|
|
1520
|
-
gitProvider.listPullReviews.mockResolvedValue([
|
|
1521
|
-
{ id: 1, body: "<!-- spaceflow-review --> old review" },
|
|
1522
|
-
{ id: 2, body: "normal review" },
|
|
1523
|
-
] as any);
|
|
1524
|
-
gitProvider.listIssueComments.mockResolvedValue([] as any);
|
|
1525
|
-
gitProvider.deletePullReview.mockResolvedValue(undefined as any);
|
|
1526
|
-
await (service as any).deleteExistingAiReviews("o", "r", 1);
|
|
1527
|
-
expect(gitProvider.deletePullReview).toHaveBeenCalledWith("o", "r", 1, 1);
|
|
1528
|
-
expect(gitProvider.deletePullReview).toHaveBeenCalledTimes(1);
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
|
-
it("should delete AI reviews via issue comment API", async () => {
|
|
1532
|
-
gitProvider.listPullReviews.mockResolvedValue([] as any);
|
|
1533
|
-
gitProvider.listIssueComments.mockResolvedValue([
|
|
1534
|
-
{ id: 10, body: "<!-- spaceflow-review --> old comment" },
|
|
1535
|
-
{ id: 11, body: "normal comment" },
|
|
1536
|
-
] as any);
|
|
1537
|
-
gitProvider.deleteIssueComment.mockResolvedValue(undefined as any);
|
|
1538
|
-
await (service as any).deleteExistingAiReviews("o", "r", 1);
|
|
1539
|
-
expect(gitProvider.deleteIssueComment).toHaveBeenCalledWith("o", "r", 10);
|
|
1540
|
-
expect(gitProvider.deleteIssueComment).toHaveBeenCalledTimes(1);
|
|
1541
|
-
});
|
|
1542
|
-
|
|
1543
|
-
it("should handle review API error gracefully", async () => {
|
|
1544
|
-
gitProvider.listPullReviews.mockRejectedValue(new Error("fail"));
|
|
1545
|
-
gitProvider.listIssueComments.mockResolvedValue([] as any);
|
|
1546
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1547
|
-
await (service as any).deleteExistingAiReviews("o", "r", 1);
|
|
1548
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
1549
|
-
consoleSpy.mockRestore();
|
|
1550
|
-
});
|
|
1551
|
-
|
|
1552
|
-
it("should handle issue comment API error gracefully", async () => {
|
|
1553
|
-
gitProvider.listPullReviews.mockResolvedValue([] as any);
|
|
1554
|
-
gitProvider.listIssueComments.mockRejectedValue(new Error("fail"));
|
|
1555
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1556
|
-
await (service as any).deleteExistingAiReviews("o", "r", 1);
|
|
1557
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
1558
|
-
consoleSpy.mockRestore();
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
it("should log error when deleting comment fails", async () => {
|
|
1562
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1563
|
-
gitProvider.listPullReviews.mockResolvedValue([] as any);
|
|
1564
|
-
gitProvider.listIssueComments.mockResolvedValue([
|
|
1565
|
-
{ id: 10, body: "<!-- spaceflow-review --> old comment" },
|
|
1566
|
-
] as any);
|
|
1567
|
-
gitProvider.deleteIssueComment.mockRejectedValue(new Error("delete failed"));
|
|
1568
|
-
|
|
1569
|
-
await (service as any).deleteExistingAiReviews("o", "r", 1);
|
|
1570
|
-
expect(consoleSpy).toHaveBeenCalledWith("⚠️ 删除评论 10 失败:", expect.any(Error));
|
|
1571
|
-
consoleSpy.mockRestore();
|
|
1572
|
-
});
|
|
1573
|
-
});
|
|
1574
|
-
|
|
1575
|
-
describe("ReviewService.invalidateIssuesForChangedFiles", () => {
|
|
1576
|
-
it("should return issues unchanged when no headSha", async () => {
|
|
1577
|
-
const issues = [{ file: "a.ts", line: "1" }];
|
|
1578
|
-
const result = await (service as any).invalidateIssuesForChangedFiles(
|
|
1579
|
-
issues,
|
|
1580
|
-
undefined,
|
|
1581
|
-
"o",
|
|
1582
|
-
"r",
|
|
1583
|
-
);
|
|
1584
|
-
expect(result).toBe(issues);
|
|
1585
|
-
});
|
|
1586
|
-
|
|
1587
|
-
it("should invalidate issues for changed files", async () => {
|
|
1588
|
-
gitProvider.getCommitDiff = vi
|
|
1589
|
-
.fn()
|
|
1590
|
-
.mockResolvedValue(
|
|
1591
|
-
"diff --git a/changed.ts b/changed.ts\n--- a/changed.ts\n+++ b/changed.ts\n@@ -1,1 +1,2 @@\n line1\n+new",
|
|
1592
|
-
) as any;
|
|
1593
|
-
const issues = [
|
|
1594
|
-
{ file: "changed.ts", line: "1", ruleId: "R1" },
|
|
1595
|
-
{ file: "unchanged.ts", line: "2", ruleId: "R2" },
|
|
1596
|
-
{ file: "changed.ts", line: "3", ruleId: "R3", fixed: "2024-01-01" },
|
|
1597
|
-
];
|
|
1598
|
-
const result = await (service as any).invalidateIssuesForChangedFiles(
|
|
1599
|
-
issues,
|
|
1600
|
-
"abc123",
|
|
1601
|
-
"o",
|
|
1602
|
-
"r",
|
|
1603
|
-
);
|
|
1604
|
-
expect(result).toHaveLength(3);
|
|
1605
|
-
expect(result[0].valid).toBe("false");
|
|
1606
|
-
expect(result[1].valid).toBeUndefined();
|
|
1607
|
-
expect(result[2].fixed).toBe("2024-01-01");
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
it("should return issues unchanged when no diff files", async () => {
|
|
1611
|
-
gitProvider.getCommitDiff = vi.fn().mockResolvedValue("") as any;
|
|
1612
|
-
const issues = [{ file: "a.ts", line: "1" }];
|
|
1613
|
-
const result = await (service as any).invalidateIssuesForChangedFiles(
|
|
1614
|
-
issues,
|
|
1615
|
-
"abc123",
|
|
1616
|
-
"o",
|
|
1617
|
-
"r",
|
|
1618
|
-
);
|
|
1619
|
-
expect(result).toBe(issues);
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
it("should handle API error gracefully", async () => {
|
|
1623
|
-
gitProvider.getCommitDiff = vi.fn().mockRejectedValue(new Error("fail")) as any;
|
|
1624
|
-
const issues = [{ file: "a.ts", line: "1" }];
|
|
1625
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1626
|
-
const result = await (service as any).invalidateIssuesForChangedFiles(
|
|
1627
|
-
issues,
|
|
1628
|
-
"abc123",
|
|
1629
|
-
"o",
|
|
1630
|
-
"r",
|
|
1631
|
-
);
|
|
1632
|
-
expect(result).toBe(issues);
|
|
1633
|
-
consoleSpy.mockRestore();
|
|
1015
|
+
describe("ReviewService.fillIssueCode", () => {
|
|
1016
|
+
beforeEach(() => {
|
|
1017
|
+
mockReviewSpecService.parseLineRange = vi.fn().mockImplementation((lineStr: string) => {
|
|
1018
|
+
const lines: number[] = [];
|
|
1019
|
+
const rangeMatch = lineStr.match(/^(\d+)-(\d+)$/);
|
|
1020
|
+
if (rangeMatch) {
|
|
1021
|
+
const start = parseInt(rangeMatch[1], 10);
|
|
1022
|
+
const end = parseInt(rangeMatch[2], 10);
|
|
1023
|
+
for (let i = start; i <= end; i++) {
|
|
1024
|
+
lines.push(i);
|
|
1025
|
+
}
|
|
1026
|
+
} else {
|
|
1027
|
+
const line = parseInt(lineStr, 10);
|
|
1028
|
+
if (!isNaN(line)) {
|
|
1029
|
+
lines.push(line);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return lines;
|
|
1033
|
+
});
|
|
1634
1034
|
});
|
|
1635
|
-
});
|
|
1636
1035
|
|
|
1637
|
-
describe("ReviewService.fillIssueCode", () => {
|
|
1638
1036
|
it("should fill code from file contents", async () => {
|
|
1639
1037
|
const issues = [{ file: "test.ts", line: "2" }];
|
|
1640
1038
|
const fileContents = new Map([
|
|
@@ -1736,246 +1134,23 @@ describe("ReviewService", () => {
|
|
|
1736
1134
|
expect(result[0].author.login).toBe("GitUser");
|
|
1737
1135
|
});
|
|
1738
1136
|
|
|
1137
|
+
it("should mark invalid when existing author but ------- commit hash", async () => {
|
|
1138
|
+
const issues = [
|
|
1139
|
+
{ file: "test.ts", line: "1", commit: "-------", author: { id: "1", login: "dev1" } },
|
|
1140
|
+
];
|
|
1141
|
+
const result = await (service as any).fillIssueAuthors(issues, [], "o", "r");
|
|
1142
|
+
expect(result[0].commit).toBeUndefined();
|
|
1143
|
+
expect(result[0].valid).toBe("false");
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1739
1146
|
it("should handle issues with ------- commit hash", async () => {
|
|
1740
1147
|
const issues = [{ file: "test.ts", line: "1", commit: "-------" }];
|
|
1741
1148
|
const commits = [
|
|
1742
1149
|
{ sha: "abc1234567890", author: { id: 1, login: "dev1" }, commit: { author: {} } },
|
|
1743
1150
|
];
|
|
1744
1151
|
const result = await (service as any).fillIssueAuthors(issues, commits, "o", "r");
|
|
1745
|
-
expect(result[0].
|
|
1746
|
-
|
|
1747
|
-
});
|
|
1748
|
-
|
|
1749
|
-
describe("ReviewService.reviewSingleFile", () => {
|
|
1750
|
-
it("should return issues from LLM stream", async () => {
|
|
1751
|
-
const llmProxy = (service as any).llmProxyService;
|
|
1752
|
-
const mockStream = (async function* () {
|
|
1753
|
-
yield {
|
|
1754
|
-
type: "result",
|
|
1755
|
-
response: {
|
|
1756
|
-
structuredOutput: {
|
|
1757
|
-
issues: [{ file: "test.ts", line: "1", ruleId: "R1", reason: "bad" }],
|
|
1758
|
-
summary: "found issues",
|
|
1759
|
-
},
|
|
1760
|
-
},
|
|
1761
|
-
};
|
|
1762
|
-
})();
|
|
1763
|
-
llmProxy.chatStream.mockReturnValue(mockStream);
|
|
1764
|
-
const filePrompt = { filename: "test.ts", systemPrompt: "sys", userPrompt: "user" };
|
|
1765
|
-
const result = await (service as any).reviewSingleFile("openai", filePrompt, 2);
|
|
1766
|
-
expect(result.issues).toHaveLength(1);
|
|
1767
|
-
expect(result.summary.file).toBe("test.ts");
|
|
1768
|
-
});
|
|
1769
|
-
|
|
1770
|
-
it("should throw on error event", async () => {
|
|
1771
|
-
const llmProxy = (service as any).llmProxyService;
|
|
1772
|
-
const mockStream = (async function* () {
|
|
1773
|
-
yield { type: "error", message: "LLM failed" };
|
|
1774
|
-
})();
|
|
1775
|
-
llmProxy.chatStream.mockReturnValue(mockStream);
|
|
1776
|
-
const filePrompt = { filename: "test.ts", systemPrompt: "sys", userPrompt: "user" };
|
|
1777
|
-
await expect((service as any).reviewSingleFile("openai", filePrompt)).rejects.toThrow(
|
|
1778
|
-
"LLM failed",
|
|
1779
|
-
);
|
|
1780
|
-
});
|
|
1781
|
-
|
|
1782
|
-
it("should return empty issues when no structured output", async () => {
|
|
1783
|
-
const llmProxy = (service as any).llmProxyService;
|
|
1784
|
-
const mockStream = (async function* () {
|
|
1785
|
-
yield { type: "result", response: {} };
|
|
1786
|
-
})();
|
|
1787
|
-
llmProxy.chatStream.mockReturnValue(mockStream);
|
|
1788
|
-
const filePrompt = { filename: "test.ts", systemPrompt: "sys", userPrompt: "user" };
|
|
1789
|
-
const result = await (service as any).reviewSingleFile("openai", filePrompt);
|
|
1790
|
-
expect(result.issues).toHaveLength(0);
|
|
1791
|
-
expect(result.summary.summary).toBe("");
|
|
1792
|
-
});
|
|
1793
|
-
});
|
|
1794
|
-
|
|
1795
|
-
describe("ReviewService.postOrUpdateReviewComment", () => {
|
|
1796
|
-
it("should post review comment", async () => {
|
|
1797
|
-
const configReader = (service as any).config;
|
|
1798
|
-
configReader.getPluginConfig.mockReturnValue({});
|
|
1799
|
-
gitProvider.listIssueComments.mockResolvedValue([] as any);
|
|
1800
|
-
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
1801
|
-
gitProvider.getPullRequest.mockResolvedValue({ head: { sha: "abc123" } } as any);
|
|
1802
|
-
gitProvider.createIssueComment.mockResolvedValue({} as any);
|
|
1803
|
-
const result = { issues: [], summary: [], round: 1 };
|
|
1804
|
-
await (service as any).postOrUpdateReviewComment("o", "r", 1, result);
|
|
1805
|
-
expect(gitProvider.createIssueComment).toHaveBeenCalled();
|
|
1806
|
-
});
|
|
1807
|
-
|
|
1808
|
-
it("should update PR title when autoUpdatePrTitle enabled", async () => {
|
|
1809
|
-
const configReader = (service as any).config;
|
|
1810
|
-
configReader.getPluginConfig.mockReturnValue({ autoUpdatePrTitle: true });
|
|
1811
|
-
gitProvider.listIssueComments.mockResolvedValue([] as any);
|
|
1812
|
-
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
1813
|
-
gitProvider.editPullRequest.mockResolvedValue({} as any);
|
|
1814
|
-
gitProvider.getPullRequest.mockResolvedValue({ head: { sha: "abc123" } } as any);
|
|
1815
|
-
gitProvider.createIssueComment.mockResolvedValue({} as any);
|
|
1816
|
-
const result = { issues: [], summary: [], round: 1, title: "New Title" };
|
|
1817
|
-
await (service as any).postOrUpdateReviewComment("o", "r", 1, result);
|
|
1818
|
-
expect(gitProvider.editPullRequest).toHaveBeenCalledWith("o", "r", 1, { title: "New Title" });
|
|
1819
|
-
});
|
|
1820
|
-
|
|
1821
|
-
it("should handle createIssueComment error gracefully", async () => {
|
|
1822
|
-
const configReader = (service as any).config;
|
|
1823
|
-
configReader.getPluginConfig.mockReturnValue({});
|
|
1824
|
-
gitProvider.listIssueComments.mockResolvedValue([] as any);
|
|
1825
|
-
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
1826
|
-
gitProvider.getPullRequest.mockResolvedValue({ head: { sha: "abc123" } } as any);
|
|
1827
|
-
gitProvider.createIssueComment.mockRejectedValue(new Error("fail") as any);
|
|
1828
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1829
|
-
const result = { issues: [], summary: [], round: 1 };
|
|
1830
|
-
await (service as any).postOrUpdateReviewComment("o", "r", 1, result);
|
|
1831
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
1832
|
-
consoleSpy.mockRestore();
|
|
1833
|
-
});
|
|
1834
|
-
|
|
1835
|
-
it("should include line comments when configured", async () => {
|
|
1836
|
-
const configReader = (service as any).config;
|
|
1837
|
-
configReader.getPluginConfig.mockReturnValue({ lineComments: true });
|
|
1838
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
1839
|
-
gitProvider.listIssueComments.mockResolvedValue([] as any);
|
|
1840
|
-
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
1841
|
-
gitProvider.getPullRequest.mockResolvedValue({ head: { sha: "abc123" } } as any);
|
|
1842
|
-
gitProvider.createIssueComment.mockResolvedValue({} as any);
|
|
1843
|
-
gitProvider.createPullReview.mockResolvedValue({} as any);
|
|
1844
|
-
const result = {
|
|
1845
|
-
issues: [
|
|
1846
|
-
{
|
|
1847
|
-
file: "test.ts",
|
|
1848
|
-
line: "10",
|
|
1849
|
-
ruleId: "R1",
|
|
1850
|
-
specFile: "s.md",
|
|
1851
|
-
reason: "r",
|
|
1852
|
-
severity: "error",
|
|
1853
|
-
round: 1,
|
|
1854
|
-
},
|
|
1855
|
-
],
|
|
1856
|
-
summary: [],
|
|
1857
|
-
round: 1,
|
|
1858
|
-
};
|
|
1859
|
-
await (service as any).postOrUpdateReviewComment("o", "r", 1, result);
|
|
1860
|
-
expect(gitProvider.createPullReview.mock.calls.length).toBeGreaterThan(0);
|
|
1861
|
-
const callArgs = gitProvider.createPullReview.mock.calls[0];
|
|
1862
|
-
expect(callArgs[3].comments.length).toBeGreaterThan(0);
|
|
1863
|
-
});
|
|
1864
|
-
});
|
|
1865
|
-
|
|
1866
|
-
describe("ReviewService.syncResolvedComments", () => {
|
|
1867
|
-
it("should mark matched issues as resolved via path:line fallback", async () => {
|
|
1868
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
1869
|
-
gitProvider.listResolvedThreads.mockResolvedValue([
|
|
1870
|
-
{ path: "test.ts", line: 10, resolvedBy: { login: "user1" } },
|
|
1871
|
-
] as any);
|
|
1872
|
-
const result = { issues: [{ file: "test.ts", line: "10", ruleId: "Rule1" }] };
|
|
1873
|
-
await (service as any).syncResolvedComments("o", "r", 1, result);
|
|
1874
|
-
expect((result.issues[0] as any).resolved).toBeDefined();
|
|
1875
|
-
expect((result.issues[0] as any).resolvedBy).toEqual({ id: undefined, login: "user1" });
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
it("should mark matched issues as resolved via issue key in body", async () => {
|
|
1879
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
1880
|
-
gitProvider.listResolvedThreads.mockResolvedValue([
|
|
1881
|
-
{
|
|
1882
|
-
path: "test.ts",
|
|
1883
|
-
line: 10,
|
|
1884
|
-
resolvedBy: { login: "user1" },
|
|
1885
|
-
body: `🟡 **问题**\n<!-- issue-key: test.ts:10:RuleA -->`,
|
|
1886
|
-
},
|
|
1887
|
-
] as any);
|
|
1888
|
-
const result = {
|
|
1889
|
-
issues: [{ file: "test.ts", line: "10", ruleId: "RuleA" }],
|
|
1890
|
-
};
|
|
1891
|
-
await (service as any).syncResolvedComments("o", "r", 1, result);
|
|
1892
|
-
expect((result.issues[0] as any).resolved).toBeDefined();
|
|
1893
|
-
expect((result.issues[0] as any).resolvedBy).toEqual({ id: undefined, login: "user1" });
|
|
1894
|
-
});
|
|
1895
|
-
|
|
1896
|
-
it("should match correct issue by issue key when multiple issues at same position", async () => {
|
|
1897
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
1898
|
-
gitProvider.listResolvedThreads.mockResolvedValue([
|
|
1899
|
-
{
|
|
1900
|
-
path: "test.ts",
|
|
1901
|
-
line: 10,
|
|
1902
|
-
resolvedBy: { login: "user1" },
|
|
1903
|
-
body: `🟡 **问题B**\n<!-- issue-key: test.ts:10:RuleB -->`,
|
|
1904
|
-
},
|
|
1905
|
-
] as any);
|
|
1906
|
-
const result = {
|
|
1907
|
-
issues: [
|
|
1908
|
-
{ file: "test.ts", line: "10", ruleId: "RuleA" } as any,
|
|
1909
|
-
{ file: "test.ts", line: "10", ruleId: "RuleB" } as any,
|
|
1910
|
-
],
|
|
1911
|
-
};
|
|
1912
|
-
await (service as any).syncResolvedComments("o", "r", 1, result);
|
|
1913
|
-
expect(result.issues[0].resolved).toBeUndefined(); // RuleA 未解决
|
|
1914
|
-
expect(result.issues[0].resolvedBy).toBeUndefined();
|
|
1915
|
-
expect(result.issues[1].resolved).toBeDefined(); // RuleB 已解决
|
|
1916
|
-
expect(result.issues[1].resolvedBy).toEqual({ id: undefined, login: "user1" });
|
|
1917
|
-
});
|
|
1918
|
-
|
|
1919
|
-
it("should skip when no resolved threads", async () => {
|
|
1920
|
-
gitProvider.listResolvedThreads.mockResolvedValue([] as any);
|
|
1921
|
-
const result = { issues: [{ file: "test.ts", line: "10", ruleId: "Rule1" }] };
|
|
1922
|
-
await (service as any).syncResolvedComments("o", "r", 1, result);
|
|
1923
|
-
expect((result.issues[0] as any).resolved).toBeUndefined();
|
|
1924
|
-
});
|
|
1925
|
-
|
|
1926
|
-
it("should skip threads without path", async () => {
|
|
1927
|
-
gitProvider.listResolvedThreads.mockResolvedValue([
|
|
1928
|
-
{ path: undefined, line: 10, resolvedBy: { login: "user1" } },
|
|
1929
|
-
] as any);
|
|
1930
|
-
const result = { issues: [{ file: "test.ts", line: "10", ruleId: "Rule1" }] };
|
|
1931
|
-
await (service as any).syncResolvedComments("o", "r", 1, result);
|
|
1932
|
-
expect((result.issues[0] as any).resolved).toBeUndefined();
|
|
1933
|
-
});
|
|
1934
|
-
|
|
1935
|
-
it("should handle error gracefully", async () => {
|
|
1936
|
-
gitProvider.listResolvedThreads.mockRejectedValue(new Error("fail"));
|
|
1937
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1938
|
-
const result = { issues: [] };
|
|
1939
|
-
await (service as any).syncResolvedComments("o", "r", 1, result);
|
|
1940
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
1941
|
-
consoleSpy.mockRestore();
|
|
1942
|
-
});
|
|
1943
|
-
});
|
|
1944
|
-
|
|
1945
|
-
describe("ReviewService.callLLM", () => {
|
|
1946
|
-
it("should aggregate results from multiple files", async () => {
|
|
1947
|
-
const llmProxy = (service as any).llmProxyService;
|
|
1948
|
-
const mockStream = (async function* () {
|
|
1949
|
-
yield {
|
|
1950
|
-
type: "result",
|
|
1951
|
-
response: {
|
|
1952
|
-
structuredOutput: {
|
|
1953
|
-
issues: [{ file: "a.ts", line: "1", ruleId: "R1", reason: "bad" }],
|
|
1954
|
-
summary: "ok",
|
|
1955
|
-
},
|
|
1956
|
-
},
|
|
1957
|
-
};
|
|
1958
|
-
})();
|
|
1959
|
-
llmProxy.chatStream.mockReturnValue(mockStream);
|
|
1960
|
-
const reviewPrompt = {
|
|
1961
|
-
filePrompts: [{ filename: "a.ts", systemPrompt: "sys", userPrompt: "user" }],
|
|
1962
|
-
};
|
|
1963
|
-
const result = await (service as any).callLLM("openai", reviewPrompt);
|
|
1964
|
-
expect(result.issues).toHaveLength(1);
|
|
1965
|
-
expect(result.summary).toHaveLength(1);
|
|
1966
|
-
});
|
|
1967
|
-
|
|
1968
|
-
it("should handle failed file review", async () => {
|
|
1969
|
-
const llmProxy = (service as any).llmProxyService;
|
|
1970
|
-
const mockStream = (async function* () {
|
|
1971
|
-
yield { type: "error", message: "LLM failed" };
|
|
1972
|
-
})();
|
|
1973
|
-
llmProxy.chatStream.mockReturnValue(mockStream);
|
|
1974
|
-
const reviewPrompt = {
|
|
1975
|
-
filePrompts: [{ filename: "a.ts", systemPrompt: "sys", userPrompt: "user" }],
|
|
1976
|
-
};
|
|
1977
|
-
const result = await (service as any).callLLM("openai", reviewPrompt);
|
|
1978
|
-
expect(result.summary[0].summary).toContain("审查失败");
|
|
1152
|
+
expect(result[0].commit).toBeUndefined();
|
|
1153
|
+
expect(result[0].valid).toBe("false");
|
|
1979
1154
|
});
|
|
1980
1155
|
});
|
|
1981
1156
|
|
|
@@ -1998,15 +1173,18 @@ describe("ReviewService", () => {
|
|
|
1998
1173
|
it("should collect and return existing review result", async () => {
|
|
1999
1174
|
const mockResult = { issues: [{ file: "a.ts", line: "1", ruleId: "R1" }], summary: [] };
|
|
2000
1175
|
const mockReviewReportService = (service as any).reviewReportService;
|
|
2001
|
-
mockReviewReportService.parseMarkdown.mockReturnValue({ result: mockResult });
|
|
2002
1176
|
mockReviewReportService.formatStatsTerminal = vi.fn().mockReturnValue("stats");
|
|
2003
|
-
gitProvider.listIssueComments.mockResolvedValue([
|
|
2004
|
-
{ id: 10, body: "<!-- spaceflow-review --> content" },
|
|
2005
|
-
] as any);
|
|
2006
1177
|
gitProvider.listPullReviews.mockResolvedValue([] as any);
|
|
2007
1178
|
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
2008
1179
|
gitProvider.getPullRequestCommits.mockResolvedValue([] as any);
|
|
2009
1180
|
gitProvider.getPullRequest.mockResolvedValue({} as any);
|
|
1181
|
+
vi.spyOn(ReviewResultModel, "loadFromPr").mockResolvedValue(
|
|
1182
|
+
ReviewResultModel.create(
|
|
1183
|
+
new PullRequestModel(gitProvider as any, "o", "r", 1),
|
|
1184
|
+
mockResult as any,
|
|
1185
|
+
(service as any).resultModelDeps,
|
|
1186
|
+
),
|
|
1187
|
+
);
|
|
2010
1188
|
const context = { owner: "o", repo: "r", prNumber: 1, ci: false, dryRun: false };
|
|
2011
1189
|
const result = await (service as any).executeCollectOnly(context);
|
|
2012
1190
|
expect(result.issues).toHaveLength(1);
|
|
@@ -2018,15 +1196,18 @@ describe("ReviewService", () => {
|
|
|
2018
1196
|
it("should route to executeCollectOnly when flush is true", async () => {
|
|
2019
1197
|
const mockResult = { issues: [{ file: "a.ts", line: "1", ruleId: "R1" }], summary: [] };
|
|
2020
1198
|
const mockReviewReportService = (service as any).reviewReportService;
|
|
2021
|
-
mockReviewReportService.parseMarkdown.mockReturnValue({ result: mockResult });
|
|
2022
1199
|
mockReviewReportService.formatStatsTerminal = vi.fn().mockReturnValue("stats");
|
|
2023
|
-
gitProvider.listIssueComments.mockResolvedValue([
|
|
2024
|
-
{ id: 10, body: "<!-- spaceflow-review --> content" },
|
|
2025
|
-
] as any);
|
|
2026
1200
|
gitProvider.listPullReviews.mockResolvedValue([] as any);
|
|
2027
1201
|
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
2028
1202
|
gitProvider.getPullRequestCommits.mockResolvedValue([] as any);
|
|
2029
1203
|
gitProvider.getPullRequest.mockResolvedValue({} as any);
|
|
1204
|
+
vi.spyOn(ReviewResultModel, "loadFromPr").mockResolvedValue(
|
|
1205
|
+
ReviewResultModel.create(
|
|
1206
|
+
new PullRequestModel(gitProvider as any, "o", "r", 1),
|
|
1207
|
+
mockResult as any,
|
|
1208
|
+
(service as any).resultModelDeps,
|
|
1209
|
+
),
|
|
1210
|
+
);
|
|
2030
1211
|
const context = {
|
|
2031
1212
|
owner: "o",
|
|
2032
1213
|
repo: "r",
|
|
@@ -2147,7 +1328,7 @@ describe("ReviewService", () => {
|
|
|
2147
1328
|
|
|
2148
1329
|
it("should auto-detect prNumber from event in CI mode", async () => {
|
|
2149
1330
|
configService.get.mockReturnValue({ repository: "owner/repo", refName: "main" });
|
|
2150
|
-
vi.spyOn(service as any, "getPrNumberFromEvent").mockResolvedValue(42);
|
|
1331
|
+
vi.spyOn((service as any).contextBuilder, "getPrNumberFromEvent").mockResolvedValue(42);
|
|
2151
1332
|
gitProvider.getPullRequest.mockResolvedValue({ title: "feat: test" } as any);
|
|
2152
1333
|
const options = { dryRun: false, ci: true, verbose: 1 };
|
|
2153
1334
|
const context = await service.getContextFromEnv(options as any);
|
|
@@ -2196,7 +1377,7 @@ describe("ReviewService", () => {
|
|
|
2196
1377
|
configService.get.mockReturnValue({ repository: "owner/repo", refName: "main" });
|
|
2197
1378
|
mockGitSdkService.getCurrentBranch.mockReturnValue("feature");
|
|
2198
1379
|
mockGitSdkService.getDefaultBranch.mockReturnValue("main");
|
|
2199
|
-
const options = { dryRun: false, ci: false, verbose: 1 };
|
|
1380
|
+
const options = { dryRun: false, ci: false, verbose: 1, local: false };
|
|
2200
1381
|
const context = await service.getContextFromEnv(options as any);
|
|
2201
1382
|
expect(context.headRef).toBe("feature");
|
|
2202
1383
|
expect(context.baseRef).toBe("main");
|
|
@@ -2206,7 +1387,7 @@ describe("ReviewService", () => {
|
|
|
2206
1387
|
configService.get.mockReturnValue({ repository: "owner/repo", refName: "main" });
|
|
2207
1388
|
mockGitSdkService.getCurrentBranch.mockReturnValue("feature");
|
|
2208
1389
|
mockGitSdkService.getDefaultBranch.mockReturnValue("main");
|
|
2209
|
-
const options = { dryRun: false, ci: false };
|
|
1390
|
+
const options = { dryRun: false, ci: false, local: false };
|
|
2210
1391
|
const context = await service.getContextFromEnv(options as any);
|
|
2211
1392
|
expect(context.headRef).toBe("feature");
|
|
2212
1393
|
expect(context.baseRef).toBe("main");
|
|
@@ -2238,203 +1419,23 @@ describe("ReviewService", () => {
|
|
|
2238
1419
|
});
|
|
2239
1420
|
});
|
|
2240
1421
|
|
|
2241
|
-
describe("ReviewService.getFileDirectoryInfo", () => {
|
|
2242
|
-
it("should return root directory marker for root files", async () => {
|
|
2243
|
-
const result = await (service as any).getFileDirectoryInfo("file.ts");
|
|
2244
|
-
expect(result).toBe("(根目录)");
|
|
2245
|
-
});
|
|
2246
|
-
});
|
|
2247
|
-
|
|
2248
1422
|
describe("ReviewService.getCommitsBetweenRefs", () => {
|
|
2249
1423
|
it("should return commits from git sdk", async () => {
|
|
2250
1424
|
mockGitSdkService.getCommitsBetweenRefs.mockResolvedValue([
|
|
2251
1425
|
{ sha: "abc", commit: { message: "fix" } },
|
|
2252
1426
|
]);
|
|
2253
1427
|
const result = await (service as any).getCommitsBetweenRefs("main", "feature");
|
|
2254
|
-
expect(result).toHaveLength(1);
|
|
2255
|
-
});
|
|
2256
|
-
});
|
|
2257
|
-
|
|
2258
|
-
describe("ReviewService.getFilesForCommit", () => {
|
|
2259
|
-
it("should return files from git sdk", async () => {
|
|
2260
|
-
mockGitSdkService.getFilesForCommit.mockResolvedValue([
|
|
2261
|
-
{ filename: "a.ts", status: "modified" },
|
|
2262
|
-
]);
|
|
2263
|
-
const result = await (service as any).getFilesForCommit("abc123");
|
|
2264
|
-
expect(result).toHaveLength(1);
|
|
2265
|
-
});
|
|
2266
|
-
});
|
|
2267
|
-
|
|
2268
|
-
describe("ReviewService.syncReactionsToIssues", () => {
|
|
2269
|
-
it("should skip when no AI review found", async () => {
|
|
2270
|
-
gitProvider.listPullReviews.mockResolvedValue([{ body: "normal" }] as any);
|
|
2271
|
-
const result = { issues: [] };
|
|
2272
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, result);
|
|
2273
|
-
expect(result.issues).toEqual([]);
|
|
2274
|
-
});
|
|
2275
|
-
|
|
2276
|
-
it("should handle error gracefully", async () => {
|
|
2277
|
-
gitProvider.listPullReviews.mockRejectedValue(new Error("fail"));
|
|
2278
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
2279
|
-
const result = { issues: [] };
|
|
2280
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, result);
|
|
2281
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
2282
|
-
consoleSpy.mockRestore();
|
|
2283
|
-
});
|
|
2284
|
-
it("should mark issue as invalid on thumbs down from reviewer", async () => {
|
|
2285
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2286
|
-
gitProvider.listPullReviews.mockResolvedValue([
|
|
2287
|
-
{ id: 1, body: "<!-- spaceflow-review-lines --> content", user: { login: "bot" } },
|
|
2288
|
-
{ id: 2, body: "LGTM", user: { login: "reviewer1" } },
|
|
2289
|
-
] as any);
|
|
2290
|
-
gitProvider.getPullRequest.mockResolvedValue({
|
|
2291
|
-
head: { sha: "abc" },
|
|
2292
|
-
requested_reviewers: [],
|
|
2293
|
-
requested_reviewers_teams: [],
|
|
2294
|
-
} as any);
|
|
2295
|
-
gitProvider.listPullReviewComments.mockResolvedValue([
|
|
2296
|
-
{ id: 100, path: "test.ts", position: 10 },
|
|
2297
|
-
] as any);
|
|
2298
|
-
gitProvider.getPullReviewCommentReactions.mockResolvedValue([
|
|
2299
|
-
{ content: "-1", user: { login: "reviewer1" } },
|
|
2300
|
-
] as any);
|
|
2301
|
-
const result = { issues: [{ file: "test.ts", line: "10", valid: "true" }] };
|
|
2302
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, result);
|
|
2303
|
-
expect(result.issues[0].valid).toBe("false");
|
|
2304
|
-
});
|
|
2305
|
-
|
|
2306
|
-
it("should add requested_reviewers to reviewers set", async () => {
|
|
2307
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2308
|
-
gitProvider.listPullReviews.mockResolvedValue([
|
|
2309
|
-
{ id: 1, body: "<!-- spaceflow-review-lines --> content", user: { login: "bot" } },
|
|
2310
|
-
] as any);
|
|
2311
|
-
gitProvider.getPullRequest.mockResolvedValue({
|
|
2312
|
-
requested_reviewers: [{ login: "req-reviewer" }],
|
|
2313
|
-
requested_reviewers_teams: [],
|
|
2314
|
-
} as any);
|
|
2315
|
-
gitProvider.listPullReviewComments.mockResolvedValue([
|
|
2316
|
-
{ id: 100, path: "test.ts", position: 10 },
|
|
2317
|
-
] as any);
|
|
2318
|
-
gitProvider.getPullReviewCommentReactions.mockResolvedValue([
|
|
2319
|
-
{ content: "-1", user: { login: "req-reviewer" } },
|
|
2320
|
-
] as any);
|
|
2321
|
-
const result = { issues: [{ file: "test.ts", line: "10", valid: "true" }] };
|
|
2322
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, result);
|
|
2323
|
-
expect(result.issues[0].valid).toBe("false");
|
|
2324
|
-
});
|
|
2325
|
-
|
|
2326
|
-
it("should skip comments without id", async () => {
|
|
2327
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2328
|
-
gitProvider.listPullReviews.mockResolvedValue([
|
|
2329
|
-
{ id: 1, body: "<!-- spaceflow-review-lines --> content" },
|
|
2330
|
-
] as any);
|
|
2331
|
-
gitProvider.getPullRequest.mockResolvedValue({
|
|
2332
|
-
requested_reviewers: [],
|
|
2333
|
-
requested_reviewers_teams: [],
|
|
2334
|
-
} as any);
|
|
2335
|
-
gitProvider.listPullReviewComments.mockResolvedValue([
|
|
2336
|
-
{ path: "test.ts", position: 10 },
|
|
2337
|
-
] as any);
|
|
2338
|
-
const result = { issues: [{ file: "test.ts", line: "10", reactions: [] }] };
|
|
2339
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, result);
|
|
2340
|
-
expect(result.issues[0].reactions).toHaveLength(0);
|
|
2341
|
-
});
|
|
2342
|
-
|
|
2343
|
-
it("should skip when reactions are empty", async () => {
|
|
2344
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2345
|
-
gitProvider.listPullReviews.mockResolvedValue([
|
|
2346
|
-
{ id: 1, body: "<!-- spaceflow-review-lines --> content" },
|
|
2347
|
-
] as any);
|
|
2348
|
-
gitProvider.getPullRequest.mockResolvedValue({
|
|
2349
|
-
requested_reviewers: [],
|
|
2350
|
-
requested_reviewers_teams: [],
|
|
2351
|
-
} as any);
|
|
2352
|
-
gitProvider.listPullReviewComments.mockResolvedValue([
|
|
2353
|
-
{ id: 100, path: "test.ts", position: 10 },
|
|
2354
|
-
] as any);
|
|
2355
|
-
gitProvider.getPullReviewCommentReactions.mockResolvedValue([] as any);
|
|
2356
|
-
const result = { issues: [{ file: "test.ts", line: "10", reactions: [] }] };
|
|
2357
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, result);
|
|
2358
|
-
expect(result.issues[0].reactions).toHaveLength(0);
|
|
2359
|
-
});
|
|
2360
|
-
|
|
2361
|
-
it("should store multiple reaction types", async () => {
|
|
2362
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2363
|
-
gitProvider.listPullReviews.mockResolvedValue([
|
|
2364
|
-
{ id: 1, body: "<!-- spaceflow-review-lines --> content" },
|
|
2365
|
-
] as any);
|
|
2366
|
-
gitProvider.getPullRequest.mockResolvedValue({
|
|
2367
|
-
requested_reviewers: [],
|
|
2368
|
-
requested_reviewers_teams: [],
|
|
2369
|
-
} as any);
|
|
2370
|
-
gitProvider.listPullReviewComments.mockResolvedValue([
|
|
2371
|
-
{ id: 100, path: "test.ts", position: 10 },
|
|
2372
|
-
] as any);
|
|
2373
|
-
gitProvider.getPullReviewCommentReactions.mockResolvedValue([
|
|
2374
|
-
{ content: "+1", user: { login: "user1" } },
|
|
2375
|
-
{ content: "+1", user: { login: "user2" } },
|
|
2376
|
-
{ content: "heart", user: { login: "user1" } },
|
|
2377
|
-
] as any);
|
|
2378
|
-
const result = { issues: [{ file: "test.ts", line: "10", reactions: [] }] };
|
|
2379
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, result);
|
|
2380
|
-
expect(result.issues[0].reactions).toHaveLength(2);
|
|
2381
|
-
});
|
|
2382
|
-
|
|
2383
|
-
it("should not mark as invalid when thumbs down from non-reviewer", async () => {
|
|
2384
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2385
|
-
gitProvider.listPullReviews.mockResolvedValue([
|
|
2386
|
-
{ id: 1, body: "<!-- spaceflow-review-lines --> content" },
|
|
2387
|
-
] as any);
|
|
2388
|
-
gitProvider.getPullRequest.mockResolvedValue({
|
|
2389
|
-
requested_reviewers: [],
|
|
2390
|
-
requested_reviewers_teams: [],
|
|
2391
|
-
} as any);
|
|
2392
|
-
gitProvider.listPullReviewComments.mockResolvedValue([
|
|
2393
|
-
{ id: 100, path: "test.ts", position: 10 },
|
|
2394
|
-
] as any);
|
|
2395
|
-
gitProvider.getPullReviewCommentReactions.mockResolvedValue([
|
|
2396
|
-
{ content: "-1", user: { login: "random-user" } },
|
|
2397
|
-
] as any);
|
|
2398
|
-
const result = { issues: [{ file: "test.ts", line: "10", valid: "true", reactions: [] }] };
|
|
2399
|
-
expect(result.issues[0].valid).toBe("true");
|
|
2400
|
-
});
|
|
2401
|
-
});
|
|
2402
|
-
|
|
2403
|
-
describe("ReviewService.buildLineReviewBody", () => {
|
|
2404
|
-
it("should include previous round summary when round > 1", () => {
|
|
2405
|
-
const issues = [
|
|
2406
|
-
{ round: 2, fixed: "2024-01-01", resolved: undefined, valid: undefined },
|
|
2407
|
-
{ round: 2, resolved: "2024-01-02", fixed: undefined, valid: undefined },
|
|
2408
|
-
{ round: 2, valid: "false", fixed: undefined, resolved: undefined },
|
|
2409
|
-
{ round: 2, fixed: undefined, resolved: undefined, valid: undefined },
|
|
2410
|
-
];
|
|
2411
|
-
const allIssues = [
|
|
2412
|
-
...issues,
|
|
2413
|
-
{ round: 1, fixed: "2024-01-01" },
|
|
2414
|
-
{ round: 1, resolved: "2024-01-02" },
|
|
2415
|
-
{ round: 1, valid: "false" },
|
|
2416
|
-
{ round: 1 },
|
|
2417
|
-
];
|
|
2418
|
-
const result = (service as any).buildLineReviewBody(issues, 2, allIssues);
|
|
2419
|
-
expect(result).toContain("Round 1 回顾");
|
|
2420
|
-
expect(result).toContain("🟢 已修复 | 1");
|
|
2421
|
-
expect(result).toContain("⚪ 已解决 | 1");
|
|
2422
|
-
expect(result).toContain("❌ 无效 | 1");
|
|
2423
|
-
expect(result).toContain("⚠️ 待处理 | 1");
|
|
2424
|
-
});
|
|
2425
|
-
|
|
2426
|
-
it("should not include previous round summary when round <= 1", () => {
|
|
2427
|
-
const issues = [{ round: 1 }];
|
|
2428
|
-
const allIssues = [{ round: 1 }];
|
|
2429
|
-
const result = (service as any).buildLineReviewBody(issues, 1, allIssues);
|
|
2430
|
-
expect(result).not.toContain("Round 1 回顾");
|
|
1428
|
+
expect(result).toHaveLength(1);
|
|
2431
1429
|
});
|
|
1430
|
+
});
|
|
2432
1431
|
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
1432
|
+
describe("ReviewService.getFilesForCommit", () => {
|
|
1433
|
+
it("should return files from git sdk", async () => {
|
|
1434
|
+
mockGitSdkService.getFilesForCommit.mockResolvedValue([
|
|
1435
|
+
{ filename: "a.ts", status: "modified" },
|
|
1436
|
+
]);
|
|
1437
|
+
const result = await (service as any).getFilesForCommit("abc123");
|
|
1438
|
+
expect(result).toHaveLength(1);
|
|
2438
1439
|
});
|
|
2439
1440
|
});
|
|
2440
1441
|
|
|
@@ -2543,225 +1544,6 @@ describe("ReviewService", () => {
|
|
|
2543
1544
|
});
|
|
2544
1545
|
});
|
|
2545
1546
|
|
|
2546
|
-
describe("ReviewService.generatePrTitle", () => {
|
|
2547
|
-
it("should generate title from LLM", async () => {
|
|
2548
|
-
const llmProxy = (service as any).llmProxyService;
|
|
2549
|
-
const mockStream = (async function* () {
|
|
2550
|
-
yield { type: "text", content: "Feat: 新功能" };
|
|
2551
|
-
})();
|
|
2552
|
-
llmProxy.chatStream.mockReturnValue(mockStream);
|
|
2553
|
-
const commits = [{ sha: "abc", commit: { message: "feat: add" } }];
|
|
2554
|
-
const changedFiles = [{ filename: "a.ts", status: "modified" }];
|
|
2555
|
-
const result = await (service as any).generatePrTitle(commits, changedFiles);
|
|
2556
|
-
expect(result).toBe("Feat: 新功能");
|
|
2557
|
-
});
|
|
2558
|
-
|
|
2559
|
-
it("should fallback on error", async () => {
|
|
2560
|
-
const llmProxy = (service as any).llmProxyService;
|
|
2561
|
-
const mockStream = (async function* () {
|
|
2562
|
-
yield { type: "error", message: "fail" };
|
|
2563
|
-
})();
|
|
2564
|
-
llmProxy.chatStream.mockReturnValue(mockStream);
|
|
2565
|
-
const commits = [{ sha: "abc", commit: { message: "feat: add feature" } }];
|
|
2566
|
-
const result = await (service as any).generatePrTitle(commits, []);
|
|
2567
|
-
expect(result).toBe("feat: add feature");
|
|
2568
|
-
});
|
|
2569
|
-
});
|
|
2570
|
-
|
|
2571
|
-
describe("ReviewService.extractIssueKeyFromBody", () => {
|
|
2572
|
-
it("should extract issue key from AI comment body", () => {
|
|
2573
|
-
const body = `🟡 **问题描述**\n- **规则**: \`Rule1\`\n<!-- issue-key: test.ts:10:Rule1 -->`;
|
|
2574
|
-
expect((service as any).extractIssueKeyFromBody(body)).toBe("test.ts:10:Rule1");
|
|
2575
|
-
});
|
|
2576
|
-
|
|
2577
|
-
it("should return null for user reply without issue key marker", () => {
|
|
2578
|
-
expect((service as any).extractIssueKeyFromBody("这个问题已经修复了")).toBeNull();
|
|
2579
|
-
expect((service as any).extractIssueKeyFromBody("")).toBeNull();
|
|
2580
|
-
});
|
|
2581
|
-
});
|
|
2582
|
-
|
|
2583
|
-
describe("ReviewService.isAiGeneratedComment", () => {
|
|
2584
|
-
it("should detect comment with issue-key marker", () => {
|
|
2585
|
-
const body = `🟡 **问题**\n<!-- issue-key: test.ts:10:Rule1 -->`;
|
|
2586
|
-
expect((service as any).isAiGeneratedComment(body)).toBe(true);
|
|
2587
|
-
});
|
|
2588
|
-
|
|
2589
|
-
it("should detect comment with structured AI format (规则 + 文件)", () => {
|
|
2590
|
-
const body = ` **魔法字符串问题**\n- **文件**: \`test.ts:64-98\`\n- **规则**: \`JsTs.Base.NoMagicStringsAndNumbers\` (来自 \`js&ts.base.md\`)`;
|
|
2591
|
-
expect((service as any).isAiGeneratedComment(body)).toBe(true);
|
|
2592
|
-
});
|
|
2593
|
-
|
|
2594
|
-
it("should return false for normal user reply", () => {
|
|
2595
|
-
expect((service as any).isAiGeneratedComment("这个问题已经修复了")).toBe(false);
|
|
2596
|
-
expect((service as any).isAiGeneratedComment("LGTM")).toBe(false);
|
|
2597
|
-
expect((service as any).isAiGeneratedComment("")).toBe(false);
|
|
2598
|
-
});
|
|
2599
|
-
|
|
2600
|
-
it("should return false for partial match (only 规则 or only 文件)", () => {
|
|
2601
|
-
expect((service as any).isAiGeneratedComment("- **规则**: something")).toBe(false);
|
|
2602
|
-
expect((service as any).isAiGeneratedComment("- **文件**: something")).toBe(false);
|
|
2603
|
-
});
|
|
2604
|
-
});
|
|
2605
|
-
|
|
2606
|
-
describe("ReviewService.syncRepliesToIssues", () => {
|
|
2607
|
-
it("should sync user replies to matched issues and filter out AI comments", async () => {
|
|
2608
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2609
|
-
const reviewComments = [
|
|
2610
|
-
{
|
|
2611
|
-
id: 1,
|
|
2612
|
-
path: "test.ts",
|
|
2613
|
-
position: 10,
|
|
2614
|
-
body: `🟡 **问题描述**\n<!-- issue-key: test.ts:10:JsTs.Base.Rule1 -->`,
|
|
2615
|
-
user: { id: 1, login: "bot" },
|
|
2616
|
-
created_at: "2024-01-01",
|
|
2617
|
-
},
|
|
2618
|
-
{
|
|
2619
|
-
id: 2,
|
|
2620
|
-
path: "test.ts",
|
|
2621
|
-
position: 10,
|
|
2622
|
-
body: "reply from user",
|
|
2623
|
-
user: { id: 2, login: "dev" },
|
|
2624
|
-
created_at: "2024-01-02",
|
|
2625
|
-
},
|
|
2626
|
-
];
|
|
2627
|
-
const result = {
|
|
2628
|
-
issues: [{ file: "test.ts", line: "10", ruleId: "JsTs.Base.Rule1", replies: [] }],
|
|
2629
|
-
};
|
|
2630
|
-
await (service as any).syncRepliesToIssues("o", "r", 1, reviewComments, result);
|
|
2631
|
-
expect(result.issues[0].replies).toHaveLength(1);
|
|
2632
|
-
expect(result.issues[0].replies[0].body).toBe("reply from user");
|
|
2633
|
-
expect(result.issues[0].replies[0].user.login).toBe("dev");
|
|
2634
|
-
});
|
|
2635
|
-
|
|
2636
|
-
it("should match user reply to correct issue by issue key when multiple issues at same position", async () => {
|
|
2637
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2638
|
-
const reviewComments = [
|
|
2639
|
-
{
|
|
2640
|
-
id: 1,
|
|
2641
|
-
path: "test.ts",
|
|
2642
|
-
position: 10,
|
|
2643
|
-
body: `🔴 **问题A**\n<!-- issue-key: test.ts:10:JsTs.Base.RuleA -->`,
|
|
2644
|
-
user: { id: 1, login: "bot" },
|
|
2645
|
-
created_at: "2024-01-01T01:00:00Z",
|
|
2646
|
-
},
|
|
2647
|
-
{
|
|
2648
|
-
id: 2,
|
|
2649
|
-
path: "test.ts",
|
|
2650
|
-
position: 10,
|
|
2651
|
-
body: `🟡 **问题B**\n<!-- issue-key: test.ts:10:JsTs.Base.RuleB -->`,
|
|
2652
|
-
user: { id: 1, login: "bot" },
|
|
2653
|
-
created_at: "2024-01-01T02:00:00Z",
|
|
2654
|
-
},
|
|
2655
|
-
{
|
|
2656
|
-
id: 3,
|
|
2657
|
-
path: "test.ts",
|
|
2658
|
-
position: 10,
|
|
2659
|
-
body: "针对问题B的回复",
|
|
2660
|
-
user: { id: 2, login: "dev" },
|
|
2661
|
-
created_at: "2024-01-01T03:00:00Z",
|
|
2662
|
-
},
|
|
2663
|
-
];
|
|
2664
|
-
const result = {
|
|
2665
|
-
issues: [
|
|
2666
|
-
{ file: "test.ts", line: "10", ruleId: "JsTs.Base.RuleA" } as any,
|
|
2667
|
-
{ file: "test.ts", line: "10", ruleId: "JsTs.Base.RuleB" } as any,
|
|
2668
|
-
],
|
|
2669
|
-
};
|
|
2670
|
-
await (service as any).syncRepliesToIssues("o", "r", 1, reviewComments, result);
|
|
2671
|
-
expect(result.issues[0].replies).toBeUndefined(); // RuleA 无回复
|
|
2672
|
-
expect(result.issues[1].replies).toHaveLength(1);
|
|
2673
|
-
expect(result.issues[1].replies[0].body).toBe("针对问题B的回复");
|
|
2674
|
-
});
|
|
2675
|
-
|
|
2676
|
-
it("should skip comments without path or position", async () => {
|
|
2677
|
-
const reviewComments = [{ id: 1, body: "no path" }];
|
|
2678
|
-
const result = { issues: [] };
|
|
2679
|
-
await (service as any).syncRepliesToIssues("o", "r", 1, reviewComments, result);
|
|
2680
|
-
expect(result.issues).toEqual([]);
|
|
2681
|
-
});
|
|
2682
|
-
|
|
2683
|
-
it("should handle error gracefully", async () => {
|
|
2684
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockImplementation(() => {
|
|
2685
|
-
throw new Error("fail");
|
|
2686
|
-
});
|
|
2687
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
2688
|
-
const reviewComments = [
|
|
2689
|
-
{ id: 1, path: "test.ts", position: 10, body: "a", created_at: "2024-01-01" },
|
|
2690
|
-
{ id: 2, path: "test.ts", position: 10, body: "b", created_at: "2024-01-02" },
|
|
2691
|
-
];
|
|
2692
|
-
const result = { issues: [{ file: "test.ts", line: "10", replies: [] }] };
|
|
2693
|
-
await (service as any).syncRepliesToIssues("o", "r", 1, reviewComments, result);
|
|
2694
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
2695
|
-
consoleSpy.mockRestore();
|
|
2696
|
-
});
|
|
2697
|
-
|
|
2698
|
-
it("should fallback to path:position match when no issue key is available", async () => {
|
|
2699
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2700
|
-
const reviewComments = [
|
|
2701
|
-
{
|
|
2702
|
-
id: 1,
|
|
2703
|
-
path: "test.ts",
|
|
2704
|
-
position: 10,
|
|
2705
|
-
body: "some comment without issue key",
|
|
2706
|
-
user: { id: 1, login: "user1" },
|
|
2707
|
-
created_at: "2024-01-01",
|
|
2708
|
-
},
|
|
2709
|
-
{
|
|
2710
|
-
id: 2,
|
|
2711
|
-
path: "test.ts",
|
|
2712
|
-
position: 10,
|
|
2713
|
-
body: "user reply",
|
|
2714
|
-
user: { id: 2, login: "user2" },
|
|
2715
|
-
created_at: "2024-01-02",
|
|
2716
|
-
},
|
|
2717
|
-
];
|
|
2718
|
-
const result = {
|
|
2719
|
-
issues: [{ file: "test.ts", line: "10", ruleId: "SomeRule" } as any],
|
|
2720
|
-
};
|
|
2721
|
-
await (service as any).syncRepliesToIssues("o", "r", 1, reviewComments, result);
|
|
2722
|
-
// 两条都不含 issue key,都会通过 fallback path:position 匹配
|
|
2723
|
-
expect(result.issues[0].replies).toHaveLength(2);
|
|
2724
|
-
});
|
|
2725
|
-
|
|
2726
|
-
it("should filter out bot comments with AI structured format but without issue-key", async () => {
|
|
2727
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockReturnValue([10]);
|
|
2728
|
-
const reviewComments = [
|
|
2729
|
-
{
|
|
2730
|
-
id: 1,
|
|
2731
|
-
path: "test.ts",
|
|
2732
|
-
position: 10,
|
|
2733
|
-
body: `🟡 **问题描述**\n<!-- issue-key: test.ts:10:JsTs.Base.ComplexFunc -->`,
|
|
2734
|
-
user: { id: 1, login: "bot" },
|
|
2735
|
-
created_at: "2024-01-01T01:00:00Z",
|
|
2736
|
-
},
|
|
2737
|
-
{
|
|
2738
|
-
id: 2,
|
|
2739
|
-
path: "test.ts",
|
|
2740
|
-
position: 10,
|
|
2741
|
-
body: ` **魔法字符串问题**\n- **文件**: \`test.ts:64-98\`\n- **规则**: \`JsTs.Base.NoMagicStringsAndNumbers\` (来自 \`js&ts.base.md\`)\n- **Commit**: 3390baa\n- **建议**:\n\`\`\`ts\nconst UNKNOWN = '未知';\n\`\`\``,
|
|
2742
|
-
user: { id: 12, login: "GiteaActions" },
|
|
2743
|
-
created_at: "2024-01-01T02:00:00Z",
|
|
2744
|
-
},
|
|
2745
|
-
{
|
|
2746
|
-
id: 3,
|
|
2747
|
-
path: "test.ts",
|
|
2748
|
-
position: 10,
|
|
2749
|
-
body: "已修复,谢谢",
|
|
2750
|
-
user: { id: 5, login: "dev" },
|
|
2751
|
-
created_at: "2024-01-01T03:00:00Z",
|
|
2752
|
-
},
|
|
2753
|
-
];
|
|
2754
|
-
const result = {
|
|
2755
|
-
issues: [{ file: "test.ts", line: "10", ruleId: "JsTs.Base.ComplexFunc" } as any],
|
|
2756
|
-
};
|
|
2757
|
-
await (service as any).syncRepliesToIssues("o", "r", 1, reviewComments, result);
|
|
2758
|
-
// bot 的结构化评论应被过滤,只保留用户的真实回复
|
|
2759
|
-
expect(result.issues[0].replies).toHaveLength(1);
|
|
2760
|
-
expect(result.issues[0].replies[0].body).toBe("已修复,谢谢");
|
|
2761
|
-
expect(result.issues[0].replies[0].user.login).toBe("dev");
|
|
2762
|
-
});
|
|
2763
|
-
});
|
|
2764
|
-
|
|
2765
1547
|
describe("ReviewService.execute - CI with existingResult", () => {
|
|
2766
1548
|
beforeEach(() => {
|
|
2767
1549
|
vi.spyOn(service as any, "runLLMReview").mockResolvedValue({
|
|
@@ -2769,16 +1551,25 @@ describe("ReviewService", () => {
|
|
|
2769
1551
|
issues: [{ file: "test.ts", line: "5", ruleId: "R1", reason: "new issue" }],
|
|
2770
1552
|
summary: [{ file: "test.ts", summary: "ok" }],
|
|
2771
1553
|
});
|
|
2772
|
-
vi.spyOn(service as any, "buildLineCommitMap").mockResolvedValue(new Map());
|
|
2773
1554
|
vi.spyOn(service as any, "getFileContents").mockResolvedValue(new Map());
|
|
2774
1555
|
});
|
|
2775
1556
|
|
|
2776
1557
|
it("should merge existing issues with new issues in CI mode", async () => {
|
|
2777
|
-
vi.spyOn(
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
1558
|
+
vi.spyOn(ReviewResultModel, "loadFromPr").mockResolvedValue(
|
|
1559
|
+
ReviewResultModel.create(
|
|
1560
|
+
new PullRequestModel(gitProvider as any, "o", "r", 1),
|
|
1561
|
+
{
|
|
1562
|
+
success: true,
|
|
1563
|
+
description: "",
|
|
1564
|
+
issues: [
|
|
1565
|
+
{ file: "old.ts", line: "1", ruleId: "R2", reason: "old issue", valid: "true" },
|
|
1566
|
+
],
|
|
1567
|
+
summary: [],
|
|
1568
|
+
round: 1,
|
|
1569
|
+
} as any,
|
|
1570
|
+
(service as any).resultModelDeps,
|
|
1571
|
+
),
|
|
1572
|
+
);
|
|
2782
1573
|
const configReader = (service as any).config;
|
|
2783
1574
|
configReader.getPluginConfig.mockReturnValue({});
|
|
2784
1575
|
gitProvider.getPullRequest.mockResolvedValue({ title: "PR", head: { sha: "abc" } } as any);
|
|
@@ -2808,11 +1599,19 @@ describe("ReviewService", () => {
|
|
|
2808
1599
|
});
|
|
2809
1600
|
|
|
2810
1601
|
it("should verify fixes when verifyFixes is true", async () => {
|
|
2811
|
-
vi.spyOn(
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
1602
|
+
vi.spyOn(ReviewResultModel, "loadFromPr").mockResolvedValue(
|
|
1603
|
+
ReviewResultModel.create(
|
|
1604
|
+
new PullRequestModel(gitProvider as any, "o", "r", 1),
|
|
1605
|
+
{
|
|
1606
|
+
success: true,
|
|
1607
|
+
description: "",
|
|
1608
|
+
issues: [{ file: "old.ts", line: "1", ruleId: "R2", reason: "old", valid: "true" }],
|
|
1609
|
+
summary: [],
|
|
1610
|
+
round: 1,
|
|
1611
|
+
} as any,
|
|
1612
|
+
(service as any).resultModelDeps,
|
|
1613
|
+
),
|
|
1614
|
+
);
|
|
2816
1615
|
const configReader = (service as any).config;
|
|
2817
1616
|
configReader.getPluginConfig.mockReturnValue({});
|
|
2818
1617
|
gitProvider.getPullRequest.mockResolvedValue({ title: "PR", head: { sha: "abc" } } as any);
|
|
@@ -2849,9 +1648,8 @@ describe("ReviewService", () => {
|
|
|
2849
1648
|
issues: [],
|
|
2850
1649
|
summary: [],
|
|
2851
1650
|
});
|
|
2852
|
-
vi.spyOn(service as any, "buildLineCommitMap").mockResolvedValue(new Map());
|
|
2853
1651
|
vi.spyOn(service as any, "getFileContents").mockResolvedValue(new Map());
|
|
2854
|
-
vi.spyOn(
|
|
1652
|
+
vi.spyOn(ReviewResultModel, "loadFromPr").mockResolvedValue(null as any);
|
|
2855
1653
|
});
|
|
2856
1654
|
|
|
2857
1655
|
it("should filter by specified commits", async () => {
|
|
@@ -3041,6 +1839,13 @@ describe("ReviewService", () => {
|
|
|
3041
1839
|
gitProvider.listIssueComments.mockResolvedValue([
|
|
3042
1840
|
{ id: 10, body: "<!-- spaceflow-review --> content" },
|
|
3043
1841
|
] as any);
|
|
1842
|
+
vi.spyOn(ReviewResultModel, "loadFromPr").mockResolvedValue(
|
|
1843
|
+
ReviewResultModel.create(
|
|
1844
|
+
new PullRequestModel(gitProvider as any, "o", "r", 1),
|
|
1845
|
+
mockResult as any,
|
|
1846
|
+
(service as any).resultModelDeps,
|
|
1847
|
+
),
|
|
1848
|
+
);
|
|
3044
1849
|
gitProvider.listPullReviews.mockResolvedValue([] as any);
|
|
3045
1850
|
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
3046
1851
|
gitProvider.getPullRequestCommits.mockResolvedValue([] as any);
|
|
@@ -3162,7 +1967,7 @@ describe("ReviewService", () => {
|
|
|
3162
1967
|
});
|
|
3163
1968
|
});
|
|
3164
1969
|
|
|
3165
|
-
describe("ReviewService.
|
|
1970
|
+
describe("ReviewService.buildBasicDescription", () => {
|
|
3166
1971
|
it("should build description from commits and files", async () => {
|
|
3167
1972
|
const llmProxy = (service as any).llmProxyService;
|
|
3168
1973
|
const mockStream = (async function* () {
|
|
@@ -3175,7 +1980,7 @@ describe("ReviewService", () => {
|
|
|
3175
1980
|
{ filename: "b.ts", status: "modified" },
|
|
3176
1981
|
{ filename: "c.ts", status: "deleted" },
|
|
3177
1982
|
];
|
|
3178
|
-
const result = await (service as any).
|
|
1983
|
+
const result = await (service as any).buildBasicDescription(commits, changedFiles);
|
|
3179
1984
|
expect(result.description).toContain("提交记录");
|
|
3180
1985
|
expect(result.description).toContain("文件变更");
|
|
3181
1986
|
expect(result.description).toContain("新增 1");
|
|
@@ -3189,47 +1994,11 @@ describe("ReviewService", () => {
|
|
|
3189
1994
|
yield { type: "text", content: "Feat: empty" };
|
|
3190
1995
|
})();
|
|
3191
1996
|
llmProxy.chatStream.mockReturnValue(mockStream);
|
|
3192
|
-
const result = await (service as any).
|
|
1997
|
+
const result = await (service as any).buildBasicDescription([], []);
|
|
3193
1998
|
expect(result.title).toBeDefined();
|
|
3194
1999
|
});
|
|
3195
2000
|
});
|
|
3196
2001
|
|
|
3197
|
-
describe("ReviewService.normalizeIssues - comma separated", () => {
|
|
3198
|
-
it("should split comma separated lines into multiple issues", () => {
|
|
3199
|
-
const issues = [
|
|
3200
|
-
{ file: "test.ts", line: "10, 20", ruleId: "R1", reason: "bad", suggestion: "fix it" },
|
|
3201
|
-
];
|
|
3202
|
-
const result = (service as any).normalizeIssues(issues);
|
|
3203
|
-
expect(result).toHaveLength(2);
|
|
3204
|
-
expect(result[0].line).toBe("10");
|
|
3205
|
-
expect(result[0].suggestion).toBe("fix it");
|
|
3206
|
-
expect(result[1].line).toBe("20");
|
|
3207
|
-
expect(result[1].suggestion).toContain("参考");
|
|
3208
|
-
});
|
|
3209
|
-
});
|
|
3210
|
-
|
|
3211
|
-
describe("ReviewService.formatReviewComment - terminal format", () => {
|
|
3212
|
-
it("should use terminal format when not CI", () => {
|
|
3213
|
-
const mockReviewReportService = (service as any).reviewReportService;
|
|
3214
|
-
mockReviewReportService.format.mockReturnValue("terminal output");
|
|
3215
|
-
const result = (service as any).formatReviewComment(
|
|
3216
|
-
{ issues: [], summary: [] },
|
|
3217
|
-
{ ci: false },
|
|
3218
|
-
);
|
|
3219
|
-
expect(result).toBe("terminal output");
|
|
3220
|
-
});
|
|
3221
|
-
|
|
3222
|
-
it("should use specified outputFormat", () => {
|
|
3223
|
-
const mockReviewReportService = (service as any).reviewReportService;
|
|
3224
|
-
mockReviewReportService.format.mockReturnValue("terminal output");
|
|
3225
|
-
const result = (service as any).formatReviewComment(
|
|
3226
|
-
{ issues: [], summary: [] },
|
|
3227
|
-
{ outputFormat: "terminal" },
|
|
3228
|
-
);
|
|
3229
|
-
expect(result).toBe("terminal output");
|
|
3230
|
-
});
|
|
3231
|
-
});
|
|
3232
|
-
|
|
3233
2002
|
describe("ReviewService.getFilesForCommit - no PR", () => {
|
|
3234
2003
|
it("should use git sdk when no prNumber", async () => {
|
|
3235
2004
|
mockGitSdkService.getFilesForCommit.mockResolvedValue(["a.ts", "b.ts"]);
|
|
@@ -3250,299 +2019,7 @@ describe("ReviewService", () => {
|
|
|
3250
2019
|
});
|
|
3251
2020
|
});
|
|
3252
2021
|
|
|
3253
|
-
describe("ReviewService.
|
|
3254
|
-
it("should build line commit map from commits", async () => {
|
|
3255
|
-
gitProvider.getCommitDiff = vi
|
|
3256
|
-
.fn()
|
|
3257
|
-
.mockResolvedValue(
|
|
3258
|
-
"diff --git a/file.ts b/file.ts\n--- a/file.ts\n+++ b/file.ts\n@@ -1,2 +1,3 @@\n line1\n+new line\n line2",
|
|
3259
|
-
) as any;
|
|
3260
|
-
const commits = [{ sha: "abc1234567890" }];
|
|
3261
|
-
const result = await (service as any).buildLineCommitMap("o", "r", commits);
|
|
3262
|
-
expect(result.has("file.ts")).toBe(true);
|
|
3263
|
-
expect(result.get("file.ts").get(2)).toBe("abc1234");
|
|
3264
|
-
});
|
|
3265
|
-
|
|
3266
|
-
it("should skip commits without sha", async () => {
|
|
3267
|
-
const commits = [{ sha: undefined }];
|
|
3268
|
-
const result = await (service as any).buildLineCommitMap("o", "r", commits);
|
|
3269
|
-
expect(result.size).toBe(0);
|
|
3270
|
-
});
|
|
3271
|
-
|
|
3272
|
-
it("should fallback to git sdk on API error", async () => {
|
|
3273
|
-
gitProvider.getCommitDiff = vi.fn().mockRejectedValue(new Error("fail")) as any;
|
|
3274
|
-
mockGitSdkService.getCommitDiff = vi.fn().mockReturnValue([]);
|
|
3275
|
-
const commits = [{ sha: "abc1234567890" }];
|
|
3276
|
-
const result = await (service as any).buildLineCommitMap("o", "r", commits);
|
|
3277
|
-
expect(mockGitSdkService.getCommitDiff).toHaveBeenCalledWith("abc1234567890");
|
|
3278
|
-
expect(result.size).toBe(0);
|
|
3279
|
-
});
|
|
3280
|
-
});
|
|
3281
|
-
|
|
3282
|
-
describe("ReviewService.invalidateIssuesForChangedFiles", () => {
|
|
3283
|
-
it("should return issues unchanged when no headSha", async () => {
|
|
3284
|
-
const issues = [{ file: "test.ts" }];
|
|
3285
|
-
const result = await (service as any).invalidateIssuesForChangedFiles(
|
|
3286
|
-
issues,
|
|
3287
|
-
undefined,
|
|
3288
|
-
"o",
|
|
3289
|
-
"r",
|
|
3290
|
-
);
|
|
3291
|
-
expect(result).toBe(issues);
|
|
3292
|
-
});
|
|
3293
|
-
|
|
3294
|
-
it("should log warning when no headSha", async () => {
|
|
3295
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3296
|
-
const issues = [{ file: "test.ts" }];
|
|
3297
|
-
await (service as any).invalidateIssuesForChangedFiles(issues, undefined, "o", "r", 1);
|
|
3298
|
-
expect(consoleSpy).toHaveBeenCalledWith(" ⚠️ 无法获取 PR head SHA,跳过变更文件检查");
|
|
3299
|
-
consoleSpy.mockRestore();
|
|
3300
|
-
});
|
|
3301
|
-
|
|
3302
|
-
it("should invalidate issues for changed files", async () => {
|
|
3303
|
-
gitProvider.getCommitDiff = vi
|
|
3304
|
-
.fn()
|
|
3305
|
-
.mockResolvedValue(
|
|
3306
|
-
"diff --git a/changed.ts b/changed.ts\n--- a/changed.ts\n+++ b/changed.ts\n@@ -1,1 +1,2 @@\n line1\n+new",
|
|
3307
|
-
) as any;
|
|
3308
|
-
const issues = [
|
|
3309
|
-
{ file: "changed.ts", line: "1", ruleId: "R1" },
|
|
3310
|
-
{ file: "unchanged.ts", line: "2", ruleId: "R2" },
|
|
3311
|
-
{ file: "changed.ts", line: "3", ruleId: "R3", fixed: "2024-01-01" },
|
|
3312
|
-
];
|
|
3313
|
-
const result = await (service as any).invalidateIssuesForChangedFiles(
|
|
3314
|
-
issues,
|
|
3315
|
-
"abc123",
|
|
3316
|
-
"o",
|
|
3317
|
-
"r",
|
|
3318
|
-
1,
|
|
3319
|
-
);
|
|
3320
|
-
expect(result).toHaveLength(3);
|
|
3321
|
-
expect(result[0].valid).toBe("false");
|
|
3322
|
-
expect(result[1].valid).toBeUndefined();
|
|
3323
|
-
expect(result[2].fixed).toBe("2024-01-01");
|
|
3324
|
-
});
|
|
3325
|
-
|
|
3326
|
-
it("should log when files are invalidated", async () => {
|
|
3327
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3328
|
-
gitProvider.getCommitDiff = vi
|
|
3329
|
-
.fn()
|
|
3330
|
-
.mockResolvedValue(
|
|
3331
|
-
"diff --git a/changed.ts b/changed.ts\n--- a/changed.ts\n+++ b/changed.ts\n@@ -1,1 +1,2 @@\n line1\n+new",
|
|
3332
|
-
) as any;
|
|
3333
|
-
const issues = [{ file: "changed.ts", line: "1", ruleId: "R1" }];
|
|
3334
|
-
await (service as any).invalidateIssuesForChangedFiles(issues, "abc123", "o", "r", 1);
|
|
3335
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
3336
|
-
" 🗑️ Issue changed.ts:1 所在文件有变更,标记为无效",
|
|
3337
|
-
);
|
|
3338
|
-
expect(consoleSpy).toHaveBeenCalledWith(" 📊 共标记 1 个历史问题为无效(文件有变更)");
|
|
3339
|
-
consoleSpy.mockRestore();
|
|
3340
|
-
});
|
|
3341
|
-
|
|
3342
|
-
it("should return issues unchanged when no diff files", async () => {
|
|
3343
|
-
gitProvider.getCommitDiff = vi.fn().mockResolvedValue("") as any;
|
|
3344
|
-
const issues = [{ file: "test.ts", line: "1" }];
|
|
3345
|
-
const result = await (service as any).invalidateIssuesForChangedFiles(
|
|
3346
|
-
issues,
|
|
3347
|
-
"abc123",
|
|
3348
|
-
"o",
|
|
3349
|
-
"r",
|
|
3350
|
-
1,
|
|
3351
|
-
);
|
|
3352
|
-
expect(result).toBe(issues);
|
|
3353
|
-
});
|
|
3354
|
-
|
|
3355
|
-
it("should log when no diff files", async () => {
|
|
3356
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3357
|
-
gitProvider.getCommitDiff = vi.fn().mockResolvedValue("") as any;
|
|
3358
|
-
const issues = [{ file: "test.ts", line: "1" }];
|
|
3359
|
-
await (service as any).invalidateIssuesForChangedFiles(issues, "abc123", "o", "r", 1);
|
|
3360
|
-
expect(consoleSpy).toHaveBeenCalledWith(" ⏭️ 最新 commit 无文件变更");
|
|
3361
|
-
consoleSpy.mockRestore();
|
|
3362
|
-
});
|
|
3363
|
-
|
|
3364
|
-
it("should handle API error gracefully", async () => {
|
|
3365
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3366
|
-
gitProvider.getCommitDiff = vi.fn().mockRejectedValue(new Error("fail")) as any;
|
|
3367
|
-
const issues = [{ file: "test.ts", line: "1" }];
|
|
3368
|
-
const result = await (service as any).invalidateIssuesForChangedFiles(
|
|
3369
|
-
issues,
|
|
3370
|
-
"abc123",
|
|
3371
|
-
"o",
|
|
3372
|
-
"r",
|
|
3373
|
-
1,
|
|
3374
|
-
);
|
|
3375
|
-
expect(result).toBe(issues);
|
|
3376
|
-
expect(consoleSpy).toHaveBeenCalledWith(" ⚠️ 获取最新 commit 变更文件失败: Error: fail");
|
|
3377
|
-
consoleSpy.mockRestore();
|
|
3378
|
-
});
|
|
3379
|
-
});
|
|
3380
|
-
|
|
3381
|
-
describe("ReviewService.updateIssueLineNumbers", () => {
|
|
3382
|
-
beforeEach(() => {
|
|
3383
|
-
mockReviewSpecService.parseLineRange = vi.fn().mockImplementation((lineStr: string) => {
|
|
3384
|
-
const lines: number[] = [];
|
|
3385
|
-
const rangeMatch = lineStr.match(/^(\d+)-(\d+)$/);
|
|
3386
|
-
if (rangeMatch) {
|
|
3387
|
-
const start = parseInt(rangeMatch[1], 10);
|
|
3388
|
-
const end = parseInt(rangeMatch[2], 10);
|
|
3389
|
-
for (let i = start; i <= end; i++) {
|
|
3390
|
-
lines.push(i);
|
|
3391
|
-
}
|
|
3392
|
-
} else {
|
|
3393
|
-
const line = parseInt(lineStr, 10);
|
|
3394
|
-
if (!isNaN(line)) {
|
|
3395
|
-
lines.push(line);
|
|
3396
|
-
}
|
|
3397
|
-
}
|
|
3398
|
-
return lines;
|
|
3399
|
-
});
|
|
3400
|
-
});
|
|
3401
|
-
|
|
3402
|
-
it("should return issues unchanged when no patch for file", () => {
|
|
3403
|
-
const issues = [{ file: "test.ts", line: "5", ruleId: "R1" }];
|
|
3404
|
-
const filePatchMap = new Map([["other.ts", "@@ -1,1 +1,2 @@\n-old1\n+new1\n+new2"]]);
|
|
3405
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
3406
|
-
expect(result).toEqual(issues);
|
|
3407
|
-
});
|
|
3408
|
-
|
|
3409
|
-
it("should skip issues that are already fixed/resolved/invalid", () => {
|
|
3410
|
-
const issues = [
|
|
3411
|
-
{ file: "test.ts", line: "5", ruleId: "R1", fixed: "2024-01-01" },
|
|
3412
|
-
{ file: "test.ts", line: "6", ruleId: "R2", resolved: "2024-01-02" },
|
|
3413
|
-
{ file: "test.ts", line: "7", ruleId: "R3", valid: "false" },
|
|
3414
|
-
];
|
|
3415
|
-
const filePatchMap = new Map([["test.ts", "@@ -1,1 +1,2 @@\n-old1\n+new1\n+new2"]]);
|
|
3416
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
3417
|
-
expect(result).toEqual(issues);
|
|
3418
|
-
});
|
|
3419
|
-
|
|
3420
|
-
it("should mark issue as invalid when line is deleted", () => {
|
|
3421
|
-
const filePatchMap = new Map([["test.ts", "@@ -1,1 +1,0 @@\n-old1"]]);
|
|
3422
|
-
const issues = [{ file: "test.ts", line: "1", ruleId: "R1" }];
|
|
3423
|
-
const result = (service as any).updateIssueLineNumbers(issues, filePatchMap);
|
|
3424
|
-
expect(result[0].valid).toBe("false");
|
|
3425
|
-
expect(result[0].originalLine).toBe("1");
|
|
3426
|
-
});
|
|
3427
|
-
|
|
3428
|
-
it("should log when line is deleted and marked invalid", () => {
|
|
3429
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3430
|
-
const filePatchMap = new Map([["test.ts", "@@ -1,1 +1,0 @@\n-old1"]]);
|
|
3431
|
-
const issues = [{ file: "test.ts", line: "1", ruleId: "R1" }];
|
|
3432
|
-
(service as any).updateIssueLineNumbers(issues, filePatchMap, 1);
|
|
3433
|
-
expect(consoleSpy).toHaveBeenCalledWith("📍 Issue test.ts:1 对应的代码已被删除,标记为无效");
|
|
3434
|
-
consoleSpy.mockRestore();
|
|
3435
|
-
});
|
|
3436
|
-
|
|
3437
|
-
it("should log when line range is collapsed to single line", () => {
|
|
3438
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3439
|
-
const filePatchMap = new Map([["test.ts", "@@ -1,2 +1,1 @@\n-old1\n-old2\n+new1"]]);
|
|
3440
|
-
const issues = [{ file: "test.ts", line: "1-2", ruleId: "R1" }];
|
|
3441
|
-
(service as any).updateIssueLineNumbers(issues, filePatchMap, 1);
|
|
3442
|
-
expect(consoleSpy).toHaveBeenCalledWith("📍 Issue 行号更新: test.ts:1-2 -> test.ts:1");
|
|
3443
|
-
consoleSpy.mockRestore();
|
|
3444
|
-
});
|
|
3445
|
-
});
|
|
3446
|
-
|
|
3447
|
-
describe("ReviewService.findExistingAiComments", () => {
|
|
3448
|
-
it("should log comments when verbose level >= 2", async () => {
|
|
3449
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3450
|
-
const mockComments = [
|
|
3451
|
-
{ id: 1, body: "test comment 1" },
|
|
3452
|
-
{ id: 2, body: "test comment 2<!-- spaceflow-review -->" },
|
|
3453
|
-
] as any;
|
|
3454
|
-
gitProvider.listIssueComments.mockResolvedValue(mockComments);
|
|
3455
|
-
|
|
3456
|
-
await (service as any).findExistingAiComments("o", "r", 1, 2);
|
|
3457
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
3458
|
-
"[findExistingAiComments] listIssueComments returned 2 comments",
|
|
3459
|
-
);
|
|
3460
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
3461
|
-
"[findExistingAiComments] comment id=1, body starts with: test comment 1",
|
|
3462
|
-
);
|
|
3463
|
-
consoleSpy.mockRestore();
|
|
3464
|
-
});
|
|
3465
|
-
|
|
3466
|
-
it("should log error when API fails", async () => {
|
|
3467
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
3468
|
-
gitProvider.listIssueComments.mockRejectedValue(new Error("API error"));
|
|
3469
|
-
|
|
3470
|
-
const result = await (service as any).findExistingAiComments("o", "r", 1);
|
|
3471
|
-
expect(result).toEqual([]);
|
|
3472
|
-
expect(consoleSpy).toHaveBeenCalledWith("[findExistingAiComments] error:", expect.any(Error));
|
|
3473
|
-
consoleSpy.mockRestore();
|
|
3474
|
-
});
|
|
3475
|
-
});
|
|
3476
|
-
|
|
3477
|
-
describe("ReviewService.syncReactionsToIssues", () => {
|
|
3478
|
-
it("should log when no AI review found", async () => {
|
|
3479
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3480
|
-
gitProvider.listPullReviews.mockResolvedValue([] as any);
|
|
3481
|
-
|
|
3482
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, { issues: [] }, 2);
|
|
3483
|
-
expect(consoleSpy).toHaveBeenCalledWith("[syncReactionsToIssues] No AI review found");
|
|
3484
|
-
consoleSpy.mockRestore();
|
|
3485
|
-
});
|
|
3486
|
-
|
|
3487
|
-
it("should log reviewers from reviews", async () => {
|
|
3488
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3489
|
-
const mockReviews = [
|
|
3490
|
-
{ user: { login: "user1" }, body: "normal review" },
|
|
3491
|
-
{ user: { login: "bot" }, body: "<!-- spaceflow-review-lines --> AI review", id: 123 },
|
|
3492
|
-
] as any;
|
|
3493
|
-
gitProvider.listPullReviews.mockResolvedValue(mockReviews);
|
|
3494
|
-
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
3495
|
-
|
|
3496
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, { issues: [] }, 2);
|
|
3497
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
3498
|
-
"[syncReactionsToIssues] reviewers from reviews: user1",
|
|
3499
|
-
);
|
|
3500
|
-
consoleSpy.mockRestore();
|
|
3501
|
-
});
|
|
3502
|
-
|
|
3503
|
-
it("should log requested reviewers and teams", async () => {
|
|
3504
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3505
|
-
const mockReviews = [
|
|
3506
|
-
{ user: { login: "bot" }, body: "<!-- spaceflow-review-lines --> AI review", id: 123 },
|
|
3507
|
-
] as any;
|
|
3508
|
-
const mockPr = {
|
|
3509
|
-
requested_reviewers: [{ login: "reviewer1" }],
|
|
3510
|
-
requested_reviewers_teams: [{ name: "team1", id: 123 }],
|
|
3511
|
-
} as any;
|
|
3512
|
-
gitProvider.listPullReviews.mockResolvedValue(mockReviews);
|
|
3513
|
-
gitProvider.getPullRequest.mockResolvedValue(mockPr);
|
|
3514
|
-
gitProvider.getTeamMembers.mockResolvedValue([{ login: "teamuser1" }]);
|
|
3515
|
-
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
3516
|
-
|
|
3517
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, { issues: [] }, 2);
|
|
3518
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
3519
|
-
"[syncReactionsToIssues] requested_reviewers: reviewer1",
|
|
3520
|
-
);
|
|
3521
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
3522
|
-
'[syncReactionsToIssues] requested_reviewers_teams: [{"name":"team1","id":123}]',
|
|
3523
|
-
);
|
|
3524
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
3525
|
-
"[syncReactionsToIssues] team team1(123) members: teamuser1",
|
|
3526
|
-
);
|
|
3527
|
-
consoleSpy.mockRestore();
|
|
3528
|
-
});
|
|
3529
|
-
|
|
3530
|
-
it("should log final reviewers", async () => {
|
|
3531
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
3532
|
-
const mockReviews = [
|
|
3533
|
-
{ user: { login: "bot" }, body: "<!-- spaceflow-review-lines --> AI review", id: 123 },
|
|
3534
|
-
] as any;
|
|
3535
|
-
gitProvider.listPullReviews.mockResolvedValue(mockReviews);
|
|
3536
|
-
gitProvider.getPullRequest.mockRejectedValue(new Error("PR not found"));
|
|
3537
|
-
gitProvider.listPullReviewComments.mockResolvedValue([] as any);
|
|
3538
|
-
|
|
3539
|
-
await (service as any).syncReactionsToIssues("o", "r", 1, { issues: [] }, 2);
|
|
3540
|
-
expect(consoleSpy).toHaveBeenCalledWith("[syncReactionsToIssues] final reviewers: ");
|
|
3541
|
-
consoleSpy.mockRestore();
|
|
3542
|
-
});
|
|
3543
|
-
});
|
|
3544
|
-
|
|
3545
|
-
describe("ReviewService.deleteExistingAiReviews", () => {
|
|
2022
|
+
describe("ReviewService.filterIssuesByValidCommits", () => {
|
|
3546
2023
|
beforeEach(() => {
|
|
3547
2024
|
mockReviewSpecService.parseLineRange = vi.fn().mockImplementation((lineStr: string) => {
|
|
3548
2025
|
const lines: number[] = [];
|