xai-review 0.26.0__py3-none-any.whl → 0.28.0__py3-none-any.whl
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.
Potentially problematic release.
This version of xai-review might be problematic. Click here for more details.
- ai_review/cli/commands/run_inline_reply_review.py +7 -0
- ai_review/cli/commands/run_summary_reply_review.py +7 -0
- ai_review/cli/main.py +17 -0
- ai_review/clients/bitbucket/pr/client.py +45 -8
- ai_review/clients/bitbucket/pr/schema/comments.py +21 -2
- ai_review/clients/bitbucket/pr/schema/files.py +8 -3
- ai_review/clients/bitbucket/pr/schema/pull_request.py +1 -5
- ai_review/clients/bitbucket/pr/schema/user.py +7 -0
- ai_review/clients/bitbucket/tools.py +6 -0
- ai_review/clients/github/pr/client.py +98 -13
- ai_review/clients/github/pr/schema/comments.py +23 -1
- ai_review/clients/github/pr/schema/files.py +2 -1
- ai_review/clients/github/pr/schema/pull_request.py +1 -4
- ai_review/clients/github/pr/schema/reviews.py +2 -1
- ai_review/clients/github/pr/schema/user.py +6 -0
- ai_review/clients/github/pr/types.py +11 -1
- ai_review/clients/github/tools.py +6 -0
- ai_review/clients/gitlab/mr/client.py +67 -7
- ai_review/clients/gitlab/mr/schema/changes.py +1 -5
- ai_review/clients/gitlab/mr/schema/discussions.py +19 -8
- ai_review/clients/gitlab/mr/schema/notes.py +5 -1
- ai_review/clients/gitlab/mr/schema/user.py +7 -0
- ai_review/clients/gitlab/mr/types.py +16 -7
- ai_review/clients/gitlab/tools.py +5 -0
- ai_review/libs/config/prompt.py +96 -64
- ai_review/libs/config/review.py +2 -0
- ai_review/libs/config/vcs/base.py +2 -0
- ai_review/libs/config/vcs/pagination.py +6 -0
- ai_review/libs/http/paginate.py +43 -0
- ai_review/libs/llm/output_json_parser.py +60 -0
- ai_review/prompts/default_inline_reply.md +10 -0
- ai_review/prompts/default_summary_reply.md +14 -0
- ai_review/prompts/default_system_inline_reply.md +31 -0
- ai_review/prompts/default_system_summary_reply.md +13 -0
- ai_review/services/artifacts/schema.py +2 -2
- ai_review/services/hook/constants.py +14 -0
- ai_review/services/hook/service.py +95 -4
- ai_review/services/hook/types.py +18 -2
- ai_review/services/prompt/adapter.py +1 -1
- ai_review/services/prompt/service.py +49 -3
- ai_review/services/prompt/tools.py +21 -0
- ai_review/services/prompt/types.py +23 -0
- ai_review/services/review/gateway/comment.py +45 -6
- ai_review/services/review/gateway/llm.py +2 -1
- ai_review/services/review/gateway/types.py +50 -0
- ai_review/services/review/internal/inline/service.py +40 -0
- ai_review/services/review/internal/inline/types.py +8 -0
- ai_review/services/review/internal/inline_reply/schema.py +23 -0
- ai_review/services/review/internal/inline_reply/service.py +20 -0
- ai_review/services/review/internal/inline_reply/types.py +8 -0
- ai_review/services/review/{policy → internal/policy}/service.py +2 -1
- ai_review/services/review/internal/policy/types.py +15 -0
- ai_review/services/review/{summary → internal/summary}/service.py +2 -2
- ai_review/services/review/{summary → internal/summary}/types.py +1 -1
- ai_review/services/review/internal/summary_reply/__init__.py +0 -0
- ai_review/services/review/internal/summary_reply/schema.py +8 -0
- ai_review/services/review/internal/summary_reply/service.py +15 -0
- ai_review/services/review/internal/summary_reply/types.py +8 -0
- ai_review/services/review/runner/__init__.py +0 -0
- ai_review/services/review/runner/context.py +72 -0
- ai_review/services/review/runner/inline.py +80 -0
- ai_review/services/review/runner/inline_reply.py +80 -0
- ai_review/services/review/runner/summary.py +71 -0
- ai_review/services/review/runner/summary_reply.py +79 -0
- ai_review/services/review/runner/types.py +6 -0
- ai_review/services/review/service.py +78 -110
- ai_review/services/vcs/bitbucket/adapter.py +24 -0
- ai_review/services/vcs/bitbucket/client.py +107 -42
- ai_review/services/vcs/github/adapter.py +35 -0
- ai_review/services/vcs/github/client.py +105 -44
- ai_review/services/vcs/gitlab/adapter.py +26 -0
- ai_review/services/vcs/gitlab/client.py +91 -38
- ai_review/services/vcs/types.py +34 -0
- ai_review/tests/fixtures/clients/bitbucket.py +2 -2
- ai_review/tests/fixtures/clients/github.py +35 -6
- ai_review/tests/fixtures/clients/gitlab.py +42 -3
- ai_review/tests/fixtures/libs/__init__.py +0 -0
- ai_review/tests/fixtures/libs/llm/__init__.py +0 -0
- ai_review/tests/fixtures/libs/llm/output_json_parser.py +13 -0
- ai_review/tests/fixtures/services/hook.py +8 -0
- ai_review/tests/fixtures/services/llm.py +8 -5
- ai_review/tests/fixtures/services/prompt.py +70 -0
- ai_review/tests/fixtures/services/review/base.py +41 -0
- ai_review/tests/fixtures/services/review/gateway/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/gateway/comment.py +98 -0
- ai_review/tests/fixtures/services/review/gateway/llm.py +17 -0
- ai_review/tests/fixtures/services/review/internal/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/{inline.py → internal/inline.py} +8 -6
- ai_review/tests/fixtures/services/review/internal/inline_reply.py +25 -0
- ai_review/tests/fixtures/services/review/internal/policy.py +28 -0
- ai_review/tests/fixtures/services/review/internal/summary.py +21 -0
- ai_review/tests/fixtures/services/review/internal/summary_reply.py +19 -0
- ai_review/tests/fixtures/services/review/runner/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/runner/context.py +50 -0
- ai_review/tests/fixtures/services/review/runner/inline.py +50 -0
- ai_review/tests/fixtures/services/review/runner/inline_reply.py +50 -0
- ai_review/tests/fixtures/services/review/runner/summary.py +50 -0
- ai_review/tests/fixtures/services/review/runner/summary_reply.py +50 -0
- ai_review/tests/fixtures/services/vcs.py +23 -0
- ai_review/tests/suites/cli/__init__.py +0 -0
- ai_review/tests/suites/cli/test_main.py +54 -0
- ai_review/tests/suites/clients/bitbucket/__init__.py +0 -0
- ai_review/tests/suites/clients/bitbucket/test_client.py +14 -0
- ai_review/tests/suites/clients/bitbucket/test_tools.py +31 -0
- ai_review/tests/suites/clients/github/test_tools.py +31 -0
- ai_review/tests/suites/clients/gitlab/test_tools.py +26 -0
- ai_review/tests/suites/libs/config/test_prompt.py +108 -28
- ai_review/tests/suites/libs/http/__init__.py +0 -0
- ai_review/tests/suites/libs/http/test_paginate.py +95 -0
- ai_review/tests/suites/libs/llm/__init__.py +0 -0
- ai_review/tests/suites/libs/llm/test_output_json_parser.py +155 -0
- ai_review/tests/suites/services/hook/test_service.py +88 -4
- ai_review/tests/suites/services/prompt/test_adapter.py +3 -3
- ai_review/tests/suites/services/prompt/test_service.py +102 -58
- ai_review/tests/suites/services/prompt/test_tools.py +86 -1
- ai_review/tests/suites/services/review/gateway/__init__.py +0 -0
- ai_review/tests/suites/services/review/gateway/test_comment.py +253 -0
- ai_review/tests/suites/services/review/gateway/test_llm.py +82 -0
- ai_review/tests/suites/services/review/internal/__init__.py +0 -0
- ai_review/tests/suites/services/review/internal/inline/__init__.py +0 -0
- ai_review/tests/suites/services/review/{inline → internal/inline}/test_schema.py +1 -1
- ai_review/tests/suites/services/review/internal/inline/test_service.py +81 -0
- ai_review/tests/suites/services/review/internal/inline_reply/__init__.py +0 -0
- ai_review/tests/suites/services/review/internal/inline_reply/test_schema.py +57 -0
- ai_review/tests/suites/services/review/internal/inline_reply/test_service.py +72 -0
- ai_review/tests/suites/services/review/internal/policy/__init__.py +0 -0
- ai_review/tests/suites/services/review/{policy → internal/policy}/test_service.py +1 -1
- ai_review/tests/suites/services/review/internal/summary/__init__.py +0 -0
- ai_review/tests/suites/services/review/{summary → internal/summary}/test_schema.py +1 -1
- ai_review/tests/suites/services/review/{summary → internal/summary}/test_service.py +2 -2
- ai_review/tests/suites/services/review/internal/summary_reply/__init__.py +0 -0
- ai_review/tests/suites/services/review/internal/summary_reply/test_schema.py +19 -0
- ai_review/tests/suites/services/review/internal/summary_reply/test_service.py +21 -0
- ai_review/tests/suites/services/review/runner/__init__.py +0 -0
- ai_review/tests/suites/services/review/runner/test_context.py +89 -0
- ai_review/tests/suites/services/review/runner/test_inline.py +100 -0
- ai_review/tests/suites/services/review/runner/test_inline_reply.py +109 -0
- ai_review/tests/suites/services/review/runner/test_summary.py +87 -0
- ai_review/tests/suites/services/review/runner/test_summary_reply.py +97 -0
- ai_review/tests/suites/services/review/test_service.py +64 -97
- ai_review/tests/suites/services/vcs/bitbucket/test_adapter.py +109 -0
- ai_review/tests/suites/services/vcs/bitbucket/{test_service.py → test_client.py} +88 -1
- ai_review/tests/suites/services/vcs/github/test_adapter.py +162 -0
- ai_review/tests/suites/services/vcs/github/{test_service.py → test_client.py} +102 -2
- ai_review/tests/suites/services/vcs/gitlab/test_adapter.py +105 -0
- ai_review/tests/suites/services/vcs/gitlab/{test_service.py → test_client.py} +99 -1
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/METADATA +8 -5
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/RECORD +160 -75
- ai_review/services/review/inline/service.py +0 -54
- ai_review/services/review/inline/types.py +0 -11
- ai_review/tests/fixtures/services/review/summary.py +0 -19
- ai_review/tests/suites/services/review/inline/test_service.py +0 -107
- /ai_review/{services/review/inline → libs/llm}/__init__.py +0 -0
- /ai_review/services/review/{policy → internal}/__init__.py +0 -0
- /ai_review/services/review/{summary → internal/inline}/__init__.py +0 -0
- /ai_review/services/review/{inline → internal/inline}/schema.py +0 -0
- /ai_review/{tests/suites/services/review/inline → services/review/internal/inline_reply}/__init__.py +0 -0
- /ai_review/{tests/suites/services/review → services/review/internal}/policy/__init__.py +0 -0
- /ai_review/{tests/suites/services/review → services/review/internal}/summary/__init__.py +0 -0
- /ai_review/services/review/{summary → internal/summary}/schema.py +0 -0
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/WHEEL +0 -0
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.26.0.dist-info → xai_review-0.28.0.dist-info}/top_level.txt +0 -0
|
@@ -1,126 +1,93 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
+
from ai_review.services.llm.types import ChatResultSchema
|
|
3
4
|
from ai_review.services.review.service import ReviewService
|
|
4
|
-
from ai_review.tests.fixtures.services.artifacts import FakeArtifactsService
|
|
5
5
|
from ai_review.tests.fixtures.services.cost import FakeCostService
|
|
6
|
-
from ai_review.tests.fixtures.services.
|
|
7
|
-
from ai_review.tests.fixtures.services.
|
|
8
|
-
from ai_review.tests.fixtures.services.
|
|
9
|
-
from ai_review.tests.fixtures.services.
|
|
10
|
-
from ai_review.tests.fixtures.services.review.
|
|
11
|
-
from ai_review.tests.fixtures.services.review.summary import FakeSummaryCommentService
|
|
12
|
-
from ai_review.tests.fixtures.services.vcs import FakeVCSClient
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@pytest.fixture
|
|
16
|
-
def review_service(
|
|
17
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
18
|
-
fake_vcs_client: FakeVCSClient,
|
|
19
|
-
fake_llm_client: FakeLLMClient,
|
|
20
|
-
fake_git_service: FakeGitService,
|
|
21
|
-
fake_diff_service: FakeDiffService,
|
|
22
|
-
fake_cost_service: FakeCostService,
|
|
23
|
-
fake_prompt_service: FakePromptService,
|
|
24
|
-
fake_artifacts_service: FakeArtifactsService,
|
|
25
|
-
fake_inline_comment_service: FakeInlineCommentService,
|
|
26
|
-
fake_summary_comment_service: FakeSummaryCommentService,
|
|
27
|
-
):
|
|
28
|
-
monkeypatch.setattr("ai_review.services.review.service.get_llm_client", lambda: fake_llm_client)
|
|
29
|
-
monkeypatch.setattr("ai_review.services.review.service.get_vcs_client", lambda: fake_vcs_client)
|
|
30
|
-
monkeypatch.setattr("ai_review.services.review.service.GitService", lambda: fake_git_service)
|
|
31
|
-
monkeypatch.setattr("ai_review.services.review.service.DiffService", lambda: fake_diff_service)
|
|
32
|
-
monkeypatch.setattr("ai_review.services.review.service.PromptService", lambda: fake_prompt_service)
|
|
33
|
-
monkeypatch.setattr("ai_review.services.review.service.InlineCommentService", lambda: fake_inline_comment_service)
|
|
34
|
-
monkeypatch.setattr("ai_review.services.review.service.SummaryCommentService", lambda: fake_summary_comment_service)
|
|
35
|
-
monkeypatch.setattr("ai_review.services.review.service.ArtifactsService", lambda: fake_artifacts_service)
|
|
36
|
-
monkeypatch.setattr("ai_review.services.review.service.CostService", lambda: fake_cost_service)
|
|
37
|
-
return ReviewService()
|
|
6
|
+
from ai_review.tests.fixtures.services.review.runner.context import FakeContextReviewRunner
|
|
7
|
+
from ai_review.tests.fixtures.services.review.runner.inline import FakeInlineReviewRunner
|
|
8
|
+
from ai_review.tests.fixtures.services.review.runner.inline_reply import FakeInlineReplyReviewRunner
|
|
9
|
+
from ai_review.tests.fixtures.services.review.runner.summary import FakeSummaryReviewRunner
|
|
10
|
+
from ai_review.tests.fixtures.services.review.runner.summary_reply import FakeSummaryReplyReviewRunner
|
|
38
11
|
|
|
39
12
|
|
|
40
13
|
@pytest.mark.asyncio
|
|
41
|
-
async def
|
|
14
|
+
async def test_run_inline_review_invokes_runner(
|
|
42
15
|
review_service: ReviewService,
|
|
43
|
-
|
|
44
|
-
fake_llm_client: FakeLLMClient,
|
|
45
|
-
fake_prompt_service: FakePromptService,
|
|
46
|
-
fake_git_service: FakeGitService,
|
|
47
|
-
fake_diff_service: FakeDiffService,
|
|
48
|
-
fake_cost_service: FakeCostService,
|
|
49
|
-
fake_artifacts_service: FakeArtifactsService,
|
|
16
|
+
fake_inline_review_runner: FakeInlineReviewRunner
|
|
50
17
|
):
|
|
51
|
-
"""Should
|
|
52
|
-
fake_git_service.responses["get_diff_for_file"] = "FAKE_DIFF"
|
|
53
|
-
|
|
18
|
+
"""Should call run() on InlineReviewRunner."""
|
|
54
19
|
await review_service.run_inline_review()
|
|
55
|
-
|
|
56
|
-
vcs_calls = [c[0] for c in fake_vcs_client.calls]
|
|
57
|
-
assert "get_review_info" in vcs_calls
|
|
58
|
-
assert "create_inline_comment" in vcs_calls
|
|
59
|
-
|
|
60
|
-
assert (
|
|
61
|
-
"get_diff_for_file",
|
|
62
|
-
{"base_sha": "A", "head_sha": "B", "file": "file.py", "unified": 3}
|
|
63
|
-
) in fake_git_service.calls
|
|
64
|
-
assert any(call[0] == "render_file" for call in fake_diff_service.calls)
|
|
65
|
-
|
|
66
|
-
assert any(call[0] == "build_inline_request" for call in fake_prompt_service.calls)
|
|
67
|
-
assert any(call[0] == "chat" for call in fake_llm_client.calls)
|
|
68
|
-
|
|
69
|
-
assert len(fake_cost_service.reports) == 1
|
|
70
|
-
assert any(call[0] == "save_llm_interaction" for call in fake_artifacts_service.calls)
|
|
20
|
+
assert fake_inline_review_runner.calls == [("run", {})]
|
|
71
21
|
|
|
72
22
|
|
|
73
23
|
@pytest.mark.asyncio
|
|
74
|
-
async def
|
|
24
|
+
async def test_run_context_review_invokes_runner(
|
|
75
25
|
review_service: ReviewService,
|
|
76
|
-
|
|
77
|
-
fake_git_service: FakeGitService,
|
|
78
|
-
fake_llm_client: FakeLLMClient,
|
|
26
|
+
fake_context_review_runner: FakeContextReviewRunner
|
|
79
27
|
):
|
|
80
|
-
"""Should
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await review_service.run_inline_review()
|
|
84
|
-
|
|
85
|
-
assert not any(call[0] == "chat" for call in fake_llm_client.calls)
|
|
86
|
-
assert not any(call[0] == "create_inline_comment" for call in fake_vcs_client.calls)
|
|
28
|
+
"""Should call run() on ContextReviewRunner."""
|
|
29
|
+
await review_service.run_context_review()
|
|
30
|
+
assert fake_context_review_runner.calls == [("run", {})]
|
|
87
31
|
|
|
88
32
|
|
|
89
33
|
@pytest.mark.asyncio
|
|
90
|
-
async def
|
|
34
|
+
async def test_run_summary_review_invokes_runner(
|
|
91
35
|
review_service: ReviewService,
|
|
92
|
-
|
|
93
|
-
fake_llm_client: FakeLLMClient,
|
|
94
|
-
fake_prompt_service: FakePromptService,
|
|
95
|
-
fake_diff_service: FakeDiffService,
|
|
36
|
+
fake_summary_review_runner: FakeSummaryReviewRunner
|
|
96
37
|
):
|
|
97
|
-
"""Should
|
|
98
|
-
await review_service.
|
|
38
|
+
"""Should call run() on SummaryReviewRunner."""
|
|
39
|
+
await review_service.run_summary_review()
|
|
40
|
+
assert fake_summary_review_runner.calls == [("run", {})]
|
|
99
41
|
|
|
100
|
-
vcs_calls = [c[0] for c in fake_vcs_client.calls]
|
|
101
|
-
assert "get_review_info" in vcs_calls
|
|
102
|
-
assert "create_inline_comment" in vcs_calls
|
|
103
42
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_run_inline_reply_review_invokes_runner(
|
|
45
|
+
review_service: ReviewService,
|
|
46
|
+
fake_inline_reply_review_runner: FakeInlineReplyReviewRunner
|
|
47
|
+
):
|
|
48
|
+
"""Should call run() on InlineReplyReviewRunner."""
|
|
49
|
+
await review_service.run_inline_reply_review()
|
|
50
|
+
assert fake_inline_reply_review_runner.calls == [("run", {})]
|
|
107
51
|
|
|
108
52
|
|
|
109
53
|
@pytest.mark.asyncio
|
|
110
|
-
async def
|
|
54
|
+
async def test_run_summary_reply_review_invokes_runner(
|
|
111
55
|
review_service: ReviewService,
|
|
112
|
-
|
|
113
|
-
fake_llm_client: FakeLLMClient,
|
|
114
|
-
fake_prompt_service: FakePromptService,
|
|
115
|
-
fake_diff_service: FakeDiffService,
|
|
56
|
+
fake_summary_reply_review_runner: FakeSummaryReplyReviewRunner
|
|
116
57
|
):
|
|
117
|
-
"""Should
|
|
118
|
-
await review_service.
|
|
58
|
+
"""Should call run() on SummaryReplyReviewRunner."""
|
|
59
|
+
await review_service.run_summary_reply_review()
|
|
60
|
+
assert fake_summary_reply_review_runner.calls == [("run", {})]
|
|
119
61
|
|
|
120
|
-
vcs_calls = [c[0] for c in fake_vcs_client.calls]
|
|
121
|
-
assert "get_review_info" in vcs_calls
|
|
122
|
-
assert "create_general_comment" in vcs_calls
|
|
123
62
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
63
|
+
def test_report_total_cost_with_data(
|
|
64
|
+
capsys,
|
|
65
|
+
review_service: ReviewService,
|
|
66
|
+
fake_cost_service: FakeCostService
|
|
67
|
+
):
|
|
68
|
+
"""Should log total cost when cost report exists."""
|
|
69
|
+
fake_cost_service.reports.append(
|
|
70
|
+
fake_cost_service.calculate(
|
|
71
|
+
result=ChatResultSchema(
|
|
72
|
+
text="result",
|
|
73
|
+
total_tokens=100,
|
|
74
|
+
prompt_tokens=50,
|
|
75
|
+
completion_tokens=10,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
review_service.report_total_cost()
|
|
81
|
+
output = capsys.readouterr().out
|
|
82
|
+
|
|
83
|
+
assert "TOTAL REVIEW COST" in output
|
|
84
|
+
assert "fake-model" in output
|
|
85
|
+
assert "0.006" in output
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_report_total_cost_no_data(capsys, review_service: ReviewService):
|
|
89
|
+
"""Should log message when no cost data is available."""
|
|
90
|
+
review_service.report_total_cost()
|
|
91
|
+
output = capsys.readouterr().out
|
|
92
|
+
|
|
93
|
+
assert "No cost data collected" in output
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from ai_review.clients.bitbucket.pr.schema.comments import (
|
|
2
|
+
BitbucketPRCommentSchema,
|
|
3
|
+
BitbucketCommentContentSchema,
|
|
4
|
+
BitbucketCommentInlineSchema,
|
|
5
|
+
BitbucketCommentParentSchema,
|
|
6
|
+
)
|
|
7
|
+
from ai_review.clients.bitbucket.pr.schema.user import BitbucketUserSchema
|
|
8
|
+
from ai_review.services.vcs.bitbucket.adapter import get_review_comment_from_bitbucket_pr_comment
|
|
9
|
+
from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_maps_all_fields_correctly():
|
|
13
|
+
"""Should map Bitbucket PR comment with all fields correctly."""
|
|
14
|
+
comment = BitbucketPRCommentSchema(
|
|
15
|
+
id=101,
|
|
16
|
+
user=BitbucketUserSchema(uuid="u-123", display_name="Alice", nickname="alice"),
|
|
17
|
+
parent=None,
|
|
18
|
+
inline=BitbucketCommentInlineSchema(path="src/utils.py", to_line=10),
|
|
19
|
+
content=BitbucketCommentContentSchema(raw="Looks good"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
result = get_review_comment_from_bitbucket_pr_comment(comment)
|
|
23
|
+
|
|
24
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
25
|
+
assert result.id == 101
|
|
26
|
+
assert result.body == "Looks good"
|
|
27
|
+
assert result.file == "src/utils.py"
|
|
28
|
+
assert result.line == 10
|
|
29
|
+
assert result.parent_id is None
|
|
30
|
+
assert result.thread_id == 101
|
|
31
|
+
|
|
32
|
+
assert isinstance(result.author, UserSchema)
|
|
33
|
+
assert result.author.id == "u-123"
|
|
34
|
+
assert result.author.name == "Alice"
|
|
35
|
+
assert result.author.username == "alice"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_maps_with_parent_comment():
|
|
39
|
+
"""Should set parent_id and use it as thread_id."""
|
|
40
|
+
comment = BitbucketPRCommentSchema(
|
|
41
|
+
id=202,
|
|
42
|
+
user=BitbucketUserSchema(uuid="u-456", display_name="Bob", nickname="bob"),
|
|
43
|
+
parent=BitbucketCommentParentSchema(id=101),
|
|
44
|
+
inline=BitbucketCommentInlineSchema(path="src/main.py", to_line=20),
|
|
45
|
+
content=BitbucketCommentContentSchema(raw="I agree"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
result = get_review_comment_from_bitbucket_pr_comment(comment)
|
|
49
|
+
|
|
50
|
+
assert result.parent_id == 101
|
|
51
|
+
assert result.thread_id == 101
|
|
52
|
+
assert result.id == 202
|
|
53
|
+
assert result.file == "src/main.py"
|
|
54
|
+
assert result.line == 20
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_maps_without_user():
|
|
58
|
+
"""Should handle missing user gracefully."""
|
|
59
|
+
comment = BitbucketPRCommentSchema(
|
|
60
|
+
id=303,
|
|
61
|
+
user=None,
|
|
62
|
+
parent=None,
|
|
63
|
+
inline=BitbucketCommentInlineSchema(path="src/app.py", to_line=5),
|
|
64
|
+
content=BitbucketCommentContentSchema(raw="Anonymous feedback"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
result = get_review_comment_from_bitbucket_pr_comment(comment)
|
|
68
|
+
|
|
69
|
+
assert isinstance(result.author, UserSchema)
|
|
70
|
+
assert result.author.id is None
|
|
71
|
+
assert result.author.name == ""
|
|
72
|
+
assert result.author.username == ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_maps_without_inline():
|
|
76
|
+
"""Should handle missing inline gracefully (file and line None)."""
|
|
77
|
+
comment = BitbucketPRCommentSchema(
|
|
78
|
+
id=404,
|
|
79
|
+
user=BitbucketUserSchema(uuid="u-789", display_name="Charlie", nickname="charlie"),
|
|
80
|
+
parent=None,
|
|
81
|
+
inline=None,
|
|
82
|
+
content=BitbucketCommentContentSchema(raw="General comment"),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
result = get_review_comment_from_bitbucket_pr_comment(comment)
|
|
86
|
+
|
|
87
|
+
assert result.file is None
|
|
88
|
+
assert result.line is None
|
|
89
|
+
assert result.thread_id == 404
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_maps_with_empty_body_and_defaults():
|
|
93
|
+
"""Should default body to empty string if content.raw is empty or None."""
|
|
94
|
+
comment = BitbucketPRCommentSchema(
|
|
95
|
+
id=505,
|
|
96
|
+
user=None,
|
|
97
|
+
parent=None,
|
|
98
|
+
inline=None,
|
|
99
|
+
content=BitbucketCommentContentSchema(raw="", html=None, markup=None),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
result = get_review_comment_from_bitbucket_pr_comment(comment)
|
|
103
|
+
|
|
104
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
105
|
+
assert result.body == ""
|
|
106
|
+
assert result.file is None
|
|
107
|
+
assert result.line is None
|
|
108
|
+
assert result.thread_id == 505
|
|
109
|
+
assert isinstance(result.author, UserSchema)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
3
|
from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
|
|
4
|
-
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema
|
|
4
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema, ReviewThreadSchema, ThreadKind
|
|
5
5
|
from ai_review.tests.fixtures.clients.bitbucket import FakeBitbucketPullRequestsHTTPClient
|
|
6
6
|
|
|
7
7
|
|
|
@@ -115,3 +115,90 @@ async def test_create_inline_comment_posts_comment(
|
|
|
115
115
|
assert call_args["content"]["raw"] == message
|
|
116
116
|
assert call_args["inline"]["path"] == file
|
|
117
117
|
assert call_args["inline"]["to"] == line
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
122
|
+
async def test_create_inline_reply_posts_comment(
|
|
123
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
124
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
125
|
+
):
|
|
126
|
+
"""Should post a reply to an existing inline thread."""
|
|
127
|
+
thread_id = 42
|
|
128
|
+
message = "I agree with this inline comment."
|
|
129
|
+
|
|
130
|
+
await bitbucket_vcs_client.create_inline_reply(thread_id, message)
|
|
131
|
+
|
|
132
|
+
calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
|
|
133
|
+
assert len(calls) == 1
|
|
134
|
+
|
|
135
|
+
call_args = calls[0]
|
|
136
|
+
assert call_args["parent"]["id"] == thread_id
|
|
137
|
+
assert call_args["content"]["raw"] == message
|
|
138
|
+
assert call_args["workspace"] == "workspace"
|
|
139
|
+
assert call_args["repo_slug"] == "repo"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.asyncio
|
|
143
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
144
|
+
async def test_create_summary_reply_posts_comment_with_parent(
|
|
145
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
146
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
147
|
+
):
|
|
148
|
+
"""Should post a reply to a general thread (same API with parent id)."""
|
|
149
|
+
thread_id = 7
|
|
150
|
+
message = "Thanks for the clarification."
|
|
151
|
+
|
|
152
|
+
await bitbucket_vcs_client.create_summary_reply(thread_id, message)
|
|
153
|
+
|
|
154
|
+
calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
|
|
155
|
+
assert len(calls) == 1
|
|
156
|
+
|
|
157
|
+
call_args = calls[0]
|
|
158
|
+
assert call_args["parent"]["id"] == thread_id
|
|
159
|
+
assert call_args["content"]["raw"] == message
|
|
160
|
+
assert call_args["workspace"] == "workspace"
|
|
161
|
+
assert call_args["repo_slug"] == "repo"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.asyncio
|
|
165
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
166
|
+
async def test_get_inline_threads_groups_by_thread_id(
|
|
167
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
168
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
169
|
+
):
|
|
170
|
+
"""Should group inline comments into threads."""
|
|
171
|
+
threads = await bitbucket_vcs_client.get_inline_threads()
|
|
172
|
+
|
|
173
|
+
assert all(isinstance(thread, ReviewThreadSchema) for thread in threads)
|
|
174
|
+
assert len(threads) == 1
|
|
175
|
+
|
|
176
|
+
thread = threads[0]
|
|
177
|
+
assert thread.kind == ThreadKind.INLINE
|
|
178
|
+
assert thread.file == "file.py"
|
|
179
|
+
assert thread.line == 5
|
|
180
|
+
assert len(thread.comments) == 1
|
|
181
|
+
assert isinstance(thread.comments[0], ReviewCommentSchema)
|
|
182
|
+
|
|
183
|
+
called_methods = [name for name, _ in fake_bitbucket_pull_requests_http_client.calls]
|
|
184
|
+
assert "get_comments" in called_methods
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.mark.asyncio
|
|
188
|
+
@pytest.mark.usefixtures("bitbucket_http_client_config")
|
|
189
|
+
async def test_get_general_threads_groups_by_thread_id(
|
|
190
|
+
bitbucket_vcs_client: BitbucketVCSClient,
|
|
191
|
+
fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
|
|
192
|
+
):
|
|
193
|
+
"""Should group general (non-inline) comments into SUMMARY threads."""
|
|
194
|
+
threads = await bitbucket_vcs_client.get_general_threads()
|
|
195
|
+
|
|
196
|
+
assert all(isinstance(t, ReviewThreadSchema) for t in threads)
|
|
197
|
+
assert len(threads) == 1
|
|
198
|
+
thread = threads[0]
|
|
199
|
+
assert thread.kind == ThreadKind.SUMMARY
|
|
200
|
+
assert len(thread.comments) == 1
|
|
201
|
+
assert isinstance(thread.comments[0], ReviewCommentSchema)
|
|
202
|
+
|
|
203
|
+
called_methods = [name for name, _ in fake_bitbucket_pull_requests_http_client.calls]
|
|
204
|
+
assert "get_comments" in called_methods
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from ai_review.clients.github.pr.schema.comments import (
|
|
2
|
+
GitHubPRCommentSchema,
|
|
3
|
+
GitHubIssueCommentSchema,
|
|
4
|
+
)
|
|
5
|
+
from ai_review.clients.github.pr.schema.user import GitHubUserSchema
|
|
6
|
+
from ai_review.services.vcs.github.adapter import (
|
|
7
|
+
get_review_comment_from_github_pr_comment,
|
|
8
|
+
get_review_comment_from_github_issue_comment,
|
|
9
|
+
)
|
|
10
|
+
from ai_review.services.vcs.types import ReviewCommentSchema, UserSchema
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_maps_all_fields_correctly_for_pr_comment():
|
|
14
|
+
"""Should map GitHub PR comment with all fields correctly."""
|
|
15
|
+
comment = GitHubPRCommentSchema(
|
|
16
|
+
id=101,
|
|
17
|
+
body="Looks fine to me",
|
|
18
|
+
path="src/utils.py",
|
|
19
|
+
line=42,
|
|
20
|
+
user=GitHubUserSchema(id=7, login="alice"),
|
|
21
|
+
in_reply_to_id=None,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
result = get_review_comment_from_github_pr_comment(comment)
|
|
25
|
+
|
|
26
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
27
|
+
assert result.id == 101
|
|
28
|
+
assert result.body == "Looks fine to me"
|
|
29
|
+
assert result.file == "src/utils.py"
|
|
30
|
+
assert result.line == 42
|
|
31
|
+
assert result.parent_id is None
|
|
32
|
+
assert result.thread_id == 101
|
|
33
|
+
|
|
34
|
+
assert isinstance(result.author, UserSchema)
|
|
35
|
+
assert result.author.id == 7
|
|
36
|
+
assert result.author.name == "alice"
|
|
37
|
+
assert result.author.username == "alice"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_maps_reply_comment_with_parent_id():
|
|
41
|
+
"""Should assign parent_id and use it as thread_id for replies."""
|
|
42
|
+
comment = GitHubPRCommentSchema(
|
|
43
|
+
id=202,
|
|
44
|
+
body="Agreed with above",
|
|
45
|
+
path="src/main.py",
|
|
46
|
+
line=10,
|
|
47
|
+
user=GitHubUserSchema(id=8, login="bob"),
|
|
48
|
+
in_reply_to_id=101,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
result = get_review_comment_from_github_pr_comment(comment)
|
|
52
|
+
|
|
53
|
+
assert result.parent_id == 101
|
|
54
|
+
assert result.thread_id == 101
|
|
55
|
+
assert result.id == 202
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_maps_comment_without_user():
|
|
59
|
+
"""Should handle missing user gracefully."""
|
|
60
|
+
comment = GitHubPRCommentSchema(
|
|
61
|
+
id=303,
|
|
62
|
+
body="Anonymous feedback",
|
|
63
|
+
path="src/app.py",
|
|
64
|
+
line=20,
|
|
65
|
+
user=None,
|
|
66
|
+
in_reply_to_id=None,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
result = get_review_comment_from_github_pr_comment(comment)
|
|
70
|
+
|
|
71
|
+
assert isinstance(result.author, UserSchema)
|
|
72
|
+
assert result.author.id is None
|
|
73
|
+
assert result.author.name == ""
|
|
74
|
+
assert result.author.username == ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_maps_comment_with_empty_body():
|
|
78
|
+
"""Should default body to empty string if it's empty or None."""
|
|
79
|
+
comment = GitHubPRCommentSchema(
|
|
80
|
+
id=404,
|
|
81
|
+
body="",
|
|
82
|
+
path=None,
|
|
83
|
+
line=None,
|
|
84
|
+
user=GitHubUserSchema(id=1, login="bot"),
|
|
85
|
+
in_reply_to_id=None,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
result = get_review_comment_from_github_pr_comment(comment)
|
|
89
|
+
|
|
90
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
91
|
+
assert result.body == ""
|
|
92
|
+
assert result.file is None
|
|
93
|
+
assert result.line is None
|
|
94
|
+
assert result.thread_id == 404
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_maps_issue_comment_all_fields():
|
|
98
|
+
"""Should map GitHub issue-level comment correctly."""
|
|
99
|
+
comment = GitHubIssueCommentSchema(
|
|
100
|
+
id=555,
|
|
101
|
+
body="Top-level discussion",
|
|
102
|
+
user=GitHubUserSchema(id=9, login="charlie"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
result = get_review_comment_from_github_issue_comment(comment)
|
|
106
|
+
|
|
107
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
108
|
+
assert result.id == 555
|
|
109
|
+
assert result.body == "Top-level discussion"
|
|
110
|
+
assert result.thread_id == 555
|
|
111
|
+
assert isinstance(result.author, UserSchema)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_maps_issue_comment_with_empty_body():
|
|
115
|
+
"""Should default empty body to empty string."""
|
|
116
|
+
comment = GitHubIssueCommentSchema(id=666, body="", user=None)
|
|
117
|
+
|
|
118
|
+
result = get_review_comment_from_github_issue_comment(comment)
|
|
119
|
+
|
|
120
|
+
assert isinstance(result, ReviewCommentSchema)
|
|
121
|
+
assert result.id == 666
|
|
122
|
+
assert result.body == ""
|
|
123
|
+
assert result.thread_id == 666
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_issue_comment_without_user_is_handled():
|
|
127
|
+
"""Should create empty UserSchema when GitHub issue comment has no user."""
|
|
128
|
+
comment = GitHubIssueCommentSchema(
|
|
129
|
+
id=777,
|
|
130
|
+
body="General feedback",
|
|
131
|
+
user=None,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
result = get_review_comment_from_github_issue_comment(comment)
|
|
135
|
+
|
|
136
|
+
assert isinstance(result.author, UserSchema)
|
|
137
|
+
assert result.author.id is None
|
|
138
|
+
assert result.author.name == ""
|
|
139
|
+
assert result.author.username == ""
|
|
140
|
+
assert result.body == "General feedback"
|
|
141
|
+
assert result.thread_id == 777
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_pr_comment_with_parent_and_missing_file_line():
|
|
145
|
+
"""Should handle replies without path/line gracefully."""
|
|
146
|
+
comment = GitHubPRCommentSchema(
|
|
147
|
+
id=999,
|
|
148
|
+
body="Follow-up question",
|
|
149
|
+
path=None,
|
|
150
|
+
line=None,
|
|
151
|
+
user=GitHubUserSchema(id=10, login="eve"),
|
|
152
|
+
in_reply_to_id=101,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
result = get_review_comment_from_github_pr_comment(comment)
|
|
156
|
+
|
|
157
|
+
assert result.parent_id == 101
|
|
158
|
+
assert result.thread_id == 101
|
|
159
|
+
assert result.file is None
|
|
160
|
+
assert result.line is None
|
|
161
|
+
assert result.body == "Follow-up question"
|
|
162
|
+
assert result.author.username == "eve"
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
3
|
from ai_review.services.vcs.github.client import GitHubVCSClient
|
|
4
|
-
from ai_review.services.vcs.types import
|
|
4
|
+
from ai_review.services.vcs.types import (
|
|
5
|
+
ThreadKind,
|
|
6
|
+
UserSchema,
|
|
7
|
+
ReviewInfoSchema,
|
|
8
|
+
ReviewThreadSchema,
|
|
9
|
+
ReviewCommentSchema,
|
|
10
|
+
)
|
|
5
11
|
from ai_review.tests.fixtures.clients.github import FakeGitHubPullRequestsHTTPClient
|
|
6
12
|
|
|
7
13
|
|
|
@@ -111,4 +117,98 @@ async def test_create_inline_comment_posts_comment(
|
|
|
111
117
|
assert call_args["path"] == "file.py"
|
|
112
118
|
assert call_args["line"] == 10
|
|
113
119
|
assert call_args["body"] == "Looks good"
|
|
114
|
-
assert call_args["commit_id"] == "def456"
|
|
120
|
+
assert call_args["commit_id"] == "def456"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
125
|
+
async def test_create_inline_reply_posts_comment(
|
|
126
|
+
github_vcs_client: GitHubVCSClient,
|
|
127
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
128
|
+
):
|
|
129
|
+
"""Should post a reply to an existing inline comment."""
|
|
130
|
+
thread_id = 3
|
|
131
|
+
message = "I agree with this suggestion."
|
|
132
|
+
|
|
133
|
+
await github_vcs_client.create_inline_reply(thread_id, message)
|
|
134
|
+
|
|
135
|
+
calls = [args for name, args in fake_github_pull_requests_http_client.calls if name == "create_review_reply"]
|
|
136
|
+
assert len(calls) == 1
|
|
137
|
+
|
|
138
|
+
call_args = calls[0]
|
|
139
|
+
assert call_args["in_reply_to"] == thread_id
|
|
140
|
+
assert call_args["body"] == message
|
|
141
|
+
assert call_args["repo"] == "repo"
|
|
142
|
+
assert call_args["owner"] == "owner"
|
|
143
|
+
assert call_args["pull_number"] == "pull_number"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.asyncio
|
|
147
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
148
|
+
async def test_create_summary_reply_reuses_general_comment_method(
|
|
149
|
+
github_vcs_client: GitHubVCSClient,
|
|
150
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
151
|
+
):
|
|
152
|
+
"""Should call create_issue_comment internally (since GitHub summary comments are flat)."""
|
|
153
|
+
thread_id = 11
|
|
154
|
+
message = "Thanks for clarifying."
|
|
155
|
+
|
|
156
|
+
await github_vcs_client.create_summary_reply(thread_id, message)
|
|
157
|
+
|
|
158
|
+
calls = [args for name, args in fake_github_pull_requests_http_client.calls if name == "create_issue_comment"]
|
|
159
|
+
assert len(calls) == 1
|
|
160
|
+
|
|
161
|
+
call_args = calls[0]
|
|
162
|
+
assert call_args["body"] == message
|
|
163
|
+
assert call_args["repo"] == "repo"
|
|
164
|
+
assert call_args["owner"] == "owner"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
169
|
+
async def test_get_inline_threads_returns_grouped_threads(
|
|
170
|
+
github_vcs_client: GitHubVCSClient,
|
|
171
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
172
|
+
):
|
|
173
|
+
"""Should group inline review comments into threads by file and line."""
|
|
174
|
+
threads = await github_vcs_client.get_inline_threads()
|
|
175
|
+
|
|
176
|
+
assert all(isinstance(t, ReviewThreadSchema) for t in threads)
|
|
177
|
+
assert len(threads) == 2 # 2 comments with unique IDs
|
|
178
|
+
|
|
179
|
+
first = threads[0]
|
|
180
|
+
assert first.kind == ThreadKind.INLINE
|
|
181
|
+
assert isinstance(first.comments[0], ReviewCommentSchema)
|
|
182
|
+
assert first.file == "file.py"
|
|
183
|
+
assert first.line == 5
|
|
184
|
+
|
|
185
|
+
called_methods = [name for name, _ in fake_github_pull_requests_http_client.calls]
|
|
186
|
+
assert "get_review_comments" in called_methods
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.asyncio
|
|
190
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
191
|
+
async def test_get_general_threads_wraps_comments_in_threads(
|
|
192
|
+
github_vcs_client: GitHubVCSClient,
|
|
193
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
194
|
+
):
|
|
195
|
+
"""Should wrap each general comment as a separate SUMMARY thread."""
|
|
196
|
+
threads = await github_vcs_client.get_general_threads()
|
|
197
|
+
|
|
198
|
+
assert all(isinstance(thread, ReviewThreadSchema) for thread in threads)
|
|
199
|
+
assert all(thread.kind == ThreadKind.SUMMARY for thread in threads)
|
|
200
|
+
assert len(threads) == 2
|
|
201
|
+
|
|
202
|
+
authors = {t.comments[0].author.username for t in threads}
|
|
203
|
+
assert authors == {"alice", "bob"}
|
|
204
|
+
|
|
205
|
+
for thread in threads:
|
|
206
|
+
comment = thread.comments[0]
|
|
207
|
+
assert isinstance(comment, ReviewCommentSchema)
|
|
208
|
+
assert isinstance(comment.author, UserSchema)
|
|
209
|
+
assert comment.author.id is not None
|
|
210
|
+
assert comment.author.username != ""
|
|
211
|
+
assert comment.thread_id == comment.id
|
|
212
|
+
|
|
213
|
+
called_methods = [name for name, _ in fake_github_pull_requests_http_client.calls]
|
|
214
|
+
assert "get_issue_comments" in called_methods
|