xai-review 0.27.0__py3-none-any.whl → 0.29.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/schema/comments.py +14 -0
- ai_review/clients/bitbucket/pr/schema/pull_request.py +1 -5
- ai_review/clients/bitbucket/pr/schema/user.py +7 -0
- ai_review/clients/github/pr/client.py +35 -4
- ai_review/clients/github/pr/schema/comments.py +21 -0
- ai_review/clients/github/pr/schema/pull_request.py +1 -4
- ai_review/clients/github/pr/schema/user.py +6 -0
- ai_review/clients/github/pr/types.py +11 -1
- ai_review/clients/gitlab/mr/client.py +32 -1
- ai_review/clients/gitlab/mr/schema/changes.py +1 -5
- ai_review/clients/gitlab/mr/schema/discussions.py +14 -12
- ai_review/clients/gitlab/mr/schema/notes.py +5 -0
- ai_review/clients/gitlab/mr/schema/position.py +13 -0
- ai_review/clients/gitlab/mr/schema/user.py +7 -0
- ai_review/clients/gitlab/mr/types.py +16 -7
- ai_review/libs/asynchronous/gather.py +8 -1
- ai_review/libs/config/prompt.py +96 -64
- ai_review/libs/config/review.py +2 -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/git/service.py +42 -11
- 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 +27 -0
- ai_review/services/vcs/bitbucket/client.py +118 -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 +28 -0
- ai_review/services/vcs/gitlab/client.py +103 -43
- 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 +71 -6
- 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/libs/config/test_prompt.py +108 -28
- 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 +134 -0
- ai_review/tests/suites/services/vcs/gitlab/{test_service.py → test_client.py} +113 -3
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/METADATA +8 -5
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/RECORD +146 -72
- 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.27.0.dist-info → xai_review-0.29.0.dist-info}/WHEEL +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.27.0.dist-info → xai_review-0.29.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.review.runner.inline_reply import InlineReplyReviewRunner
|
|
4
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewThreadSchema, ReviewCommentSchema, ThreadKind
|
|
5
|
+
from ai_review.tests.fixtures.services.cost import FakeCostService
|
|
6
|
+
from ai_review.tests.fixtures.services.diff import FakeDiffService
|
|
7
|
+
from ai_review.tests.fixtures.services.git import FakeGitService
|
|
8
|
+
from ai_review.tests.fixtures.services.prompt import FakePromptService
|
|
9
|
+
from ai_review.tests.fixtures.services.review.gateway.comment import FakeReviewCommentGateway
|
|
10
|
+
from ai_review.tests.fixtures.services.review.gateway.llm import FakeReviewLLMGateway
|
|
11
|
+
from ai_review.tests.fixtures.services.review.internal.inline_reply import FakeInlineCommentReplyService
|
|
12
|
+
from ai_review.tests.fixtures.services.vcs import FakeVCSClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_run_happy_path(
|
|
17
|
+
inline_reply_review_runner: InlineReplyReviewRunner,
|
|
18
|
+
fake_vcs_client: FakeVCSClient,
|
|
19
|
+
fake_git_service: FakeGitService,
|
|
20
|
+
fake_cost_service: FakeCostService,
|
|
21
|
+
fake_diff_service: FakeDiffService,
|
|
22
|
+
fake_prompt_service: FakePromptService,
|
|
23
|
+
fake_review_llm_gateway: FakeReviewLLMGateway,
|
|
24
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
25
|
+
):
|
|
26
|
+
"""Should process all threads, call LLM, and post replies."""
|
|
27
|
+
fake_git_service.responses["get_diff_for_file"] = "FAKE_DIFF"
|
|
28
|
+
|
|
29
|
+
await inline_reply_review_runner.run()
|
|
30
|
+
|
|
31
|
+
vcs_calls = [call[0] for call in fake_vcs_client.calls]
|
|
32
|
+
assert "get_review_info" in vcs_calls
|
|
33
|
+
|
|
34
|
+
assert any(call[0] == "get_inline_threads" for call in fake_review_comment_gateway.calls)
|
|
35
|
+
assert any(call[0] == "get_diff_for_file" for call in fake_git_service.calls)
|
|
36
|
+
assert any(call[0] == "render_file" for call in fake_diff_service.calls)
|
|
37
|
+
assert any(call[0] == "build_inline_reply_request" for call in fake_prompt_service.calls)
|
|
38
|
+
assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
|
|
39
|
+
assert any(call[0] == "process_inline_reply" for call in fake_review_comment_gateway.calls)
|
|
40
|
+
assert any(call[0] == "aggregate" for call in fake_cost_service.calls)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_run_skips_when_no_threads(
|
|
45
|
+
fake_vcs_client: FakeVCSClient,
|
|
46
|
+
inline_reply_review_runner: InlineReplyReviewRunner,
|
|
47
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
48
|
+
):
|
|
49
|
+
"""Should skip when there are no AI inline threads."""
|
|
50
|
+
fake_review_comment_gateway.responses["get_inline_threads"] = []
|
|
51
|
+
|
|
52
|
+
await inline_reply_review_runner.run()
|
|
53
|
+
|
|
54
|
+
vcs_calls = [call[0] for call in fake_vcs_client.calls]
|
|
55
|
+
assert "get_review_info" in vcs_calls
|
|
56
|
+
assert any(call[0] == "get_inline_threads" for call in fake_review_comment_gateway.calls)
|
|
57
|
+
assert not any(call[0] == "process_inline_reply" for call in fake_review_comment_gateway.calls)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_process_thread_reply_skips_when_no_diff(
|
|
62
|
+
inline_reply_review_runner: InlineReplyReviewRunner,
|
|
63
|
+
fake_git_service: FakeGitService,
|
|
64
|
+
fake_review_llm_gateway: FakeReviewLLMGateway,
|
|
65
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
66
|
+
):
|
|
67
|
+
"""Should skip reply processing when no diff found for file."""
|
|
68
|
+
fake_git_service.responses["get_diff_for_file"] = ""
|
|
69
|
+
|
|
70
|
+
review_info = ReviewInfoSchema(base_sha="A", head_sha="B")
|
|
71
|
+
thread = ReviewThreadSchema(
|
|
72
|
+
id="1",
|
|
73
|
+
kind=ThreadKind.INLINE,
|
|
74
|
+
file="file.py",
|
|
75
|
+
line=1,
|
|
76
|
+
comments=[ReviewCommentSchema(id="c1", body="Some comment")]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
await inline_reply_review_runner.process_thread_reply(thread, review_info)
|
|
80
|
+
|
|
81
|
+
assert not any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
|
|
82
|
+
assert not any(call[0] == "process_inline_reply" for call in fake_review_comment_gateway.calls)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
async def test_process_thread_reply_skips_when_no_reply(
|
|
87
|
+
inline_reply_review_runner: InlineReplyReviewRunner,
|
|
88
|
+
fake_git_service: FakeGitService,
|
|
89
|
+
fake_review_llm_gateway: FakeReviewLLMGateway,
|
|
90
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
91
|
+
fake_inline_comment_reply_service: FakeInlineCommentReplyService,
|
|
92
|
+
):
|
|
93
|
+
"""Should not post reply if model output produces no reply schema."""
|
|
94
|
+
fake_git_service.responses["get_diff_for_file"] = "SOME_DIFF"
|
|
95
|
+
fake_inline_comment_reply_service.reply = None
|
|
96
|
+
|
|
97
|
+
review_info = ReviewInfoSchema(base_sha="A", head_sha="B")
|
|
98
|
+
thread = ReviewThreadSchema(
|
|
99
|
+
id="42",
|
|
100
|
+
kind=ThreadKind.INLINE,
|
|
101
|
+
file="main.py",
|
|
102
|
+
line=12,
|
|
103
|
+
comments=[ReviewCommentSchema(id="cm1", body="Fix this!")]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
await inline_reply_review_runner.process_thread_reply(thread, review_info)
|
|
107
|
+
|
|
108
|
+
assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
|
|
109
|
+
assert not any(call[0] == "process_inline_reply" for call in fake_review_comment_gateway.calls)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.review.internal.summary.schema import SummaryCommentSchema
|
|
4
|
+
from ai_review.services.review.runner.summary import SummaryReviewRunner
|
|
5
|
+
from ai_review.tests.fixtures.services.cost import FakeCostService
|
|
6
|
+
from ai_review.tests.fixtures.services.diff import FakeDiffService
|
|
7
|
+
from ai_review.tests.fixtures.services.prompt import FakePromptService
|
|
8
|
+
from ai_review.tests.fixtures.services.review.gateway.comment import FakeReviewCommentGateway
|
|
9
|
+
from ai_review.tests.fixtures.services.review.gateway.llm import FakeReviewLLMGateway
|
|
10
|
+
from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
|
|
11
|
+
from ai_review.tests.fixtures.services.review.internal.summary import FakeSummaryCommentService
|
|
12
|
+
from ai_review.tests.fixtures.services.vcs import FakeVCSClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_run_happy_path(
|
|
17
|
+
summary_review_runner: SummaryReviewRunner,
|
|
18
|
+
fake_vcs_client: FakeVCSClient,
|
|
19
|
+
fake_diff_service: FakeDiffService,
|
|
20
|
+
fake_cost_service: FakeCostService,
|
|
21
|
+
fake_prompt_service: FakePromptService,
|
|
22
|
+
fake_review_llm_gateway: FakeReviewLLMGateway,
|
|
23
|
+
fake_review_policy_service: FakeReviewPolicyService,
|
|
24
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
25
|
+
):
|
|
26
|
+
"""Should render all changed files, call LLM and post summary comment."""
|
|
27
|
+
await summary_review_runner.run()
|
|
28
|
+
|
|
29
|
+
vcs_calls = [call[0] for call in fake_vcs_client.calls]
|
|
30
|
+
assert "get_review_info" in vcs_calls
|
|
31
|
+
|
|
32
|
+
assert any(call[0] == "render_files" for call in fake_diff_service.calls)
|
|
33
|
+
assert any(call[0] == "apply_for_files" for call in fake_review_policy_service.calls)
|
|
34
|
+
assert any(call[0] == "build_summary_request" for call in fake_prompt_service.calls)
|
|
35
|
+
assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
|
|
36
|
+
assert any(call[0] == "process_summary_comment" for call in fake_review_comment_gateway.calls)
|
|
37
|
+
|
|
38
|
+
assert any(call[0] == "aggregate" for call in fake_cost_service.calls)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_run_skips_when_existing_summary_comments(
|
|
43
|
+
summary_review_runner: SummaryReviewRunner,
|
|
44
|
+
fake_vcs_client: FakeVCSClient,
|
|
45
|
+
fake_review_llm_gateway: FakeReviewLLMGateway,
|
|
46
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
47
|
+
):
|
|
48
|
+
"""Should skip summary review if summary comment already exists."""
|
|
49
|
+
fake_review_comment_gateway.responses["has_existing_summary_comments"] = True
|
|
50
|
+
|
|
51
|
+
await summary_review_runner.run()
|
|
52
|
+
|
|
53
|
+
vcs_calls = [call[0] for call in fake_vcs_client.calls]
|
|
54
|
+
assert vcs_calls == []
|
|
55
|
+
assert not any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_run_skips_when_no_changed_files(
|
|
60
|
+
summary_review_runner: SummaryReviewRunner,
|
|
61
|
+
fake_vcs_client: FakeVCSClient,
|
|
62
|
+
fake_review_policy_service: FakeReviewPolicyService,
|
|
63
|
+
):
|
|
64
|
+
"""Should skip when no changed files remain after policy filtering."""
|
|
65
|
+
fake_review_policy_service.responses["apply_for_files"] = []
|
|
66
|
+
|
|
67
|
+
await summary_review_runner.run()
|
|
68
|
+
|
|
69
|
+
vcs_calls = [call[0] for call in fake_vcs_client.calls]
|
|
70
|
+
assert "get_review_info" in vcs_calls
|
|
71
|
+
assert any(call[0] == "apply_for_files" for call in fake_review_policy_service.calls)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_run_skips_when_empty_summary_from_llm(
|
|
76
|
+
summary_review_runner: SummaryReviewRunner,
|
|
77
|
+
fake_review_llm_gateway: FakeReviewLLMGateway,
|
|
78
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
79
|
+
fake_summary_comment_service: FakeSummaryCommentService,
|
|
80
|
+
):
|
|
81
|
+
"""Should skip posting comment if LLM output is empty."""
|
|
82
|
+
fake_summary_comment_service.responses["parse_model_output"] = SummaryCommentSchema(text="")
|
|
83
|
+
|
|
84
|
+
await summary_review_runner.run()
|
|
85
|
+
|
|
86
|
+
assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
|
|
87
|
+
assert not any(call[0] == "process_summary_comment" for call in fake_review_comment_gateway.calls)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.review.runner.summary_reply import SummaryReplyReviewRunner
|
|
4
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewThreadSchema, ReviewCommentSchema, ThreadKind
|
|
5
|
+
from ai_review.tests.fixtures.services.cost import FakeCostService
|
|
6
|
+
from ai_review.tests.fixtures.services.diff import FakeDiffService
|
|
7
|
+
from ai_review.tests.fixtures.services.prompt import FakePromptService
|
|
8
|
+
from ai_review.tests.fixtures.services.review.gateway.comment import FakeReviewCommentGateway
|
|
9
|
+
from ai_review.tests.fixtures.services.review.gateway.llm import FakeReviewLLMGateway
|
|
10
|
+
from ai_review.tests.fixtures.services.review.internal.policy import FakeReviewPolicyService
|
|
11
|
+
from ai_review.tests.fixtures.services.review.internal.summary_reply import FakeSummaryCommentReplyService
|
|
12
|
+
from ai_review.tests.fixtures.services.vcs import FakeVCSClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_run_happy_path(
|
|
17
|
+
summary_reply_review_runner: SummaryReplyReviewRunner,
|
|
18
|
+
fake_vcs_client: FakeVCSClient,
|
|
19
|
+
fake_diff_service: FakeDiffService,
|
|
20
|
+
fake_cost_service: FakeCostService,
|
|
21
|
+
fake_prompt_service: FakePromptService,
|
|
22
|
+
fake_review_llm_gateway: FakeReviewLLMGateway,
|
|
23
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
24
|
+
):
|
|
25
|
+
"""Should process all summary threads, call LLM, and post replies."""
|
|
26
|
+
await summary_reply_review_runner.run()
|
|
27
|
+
|
|
28
|
+
vcs_calls = [call[0] for call in fake_vcs_client.calls]
|
|
29
|
+
assert "get_review_info" in vcs_calls
|
|
30
|
+
|
|
31
|
+
assert any(call[0] == "get_summary_threads" for call in fake_review_comment_gateway.calls)
|
|
32
|
+
assert any(call[0] == "render_files" for call in fake_diff_service.calls)
|
|
33
|
+
assert any(call[0] == "build_summary_reply_request" for call in fake_prompt_service.calls)
|
|
34
|
+
assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
|
|
35
|
+
assert any(call[0] == "process_summary_reply" for call in fake_review_comment_gateway.calls)
|
|
36
|
+
assert any(call[0] == "aggregate" for call in fake_cost_service.calls)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_run_skips_when_no_threads(
|
|
41
|
+
summary_reply_review_runner: SummaryReplyReviewRunner,
|
|
42
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
43
|
+
fake_vcs_client: FakeVCSClient,
|
|
44
|
+
):
|
|
45
|
+
"""Should skip when there are no AI summary threads."""
|
|
46
|
+
fake_review_comment_gateway.responses["get_summary_threads"] = []
|
|
47
|
+
|
|
48
|
+
await summary_reply_review_runner.run()
|
|
49
|
+
|
|
50
|
+
vcs_calls = [call[0] for call in fake_vcs_client.calls]
|
|
51
|
+
assert "get_review_info" in vcs_calls
|
|
52
|
+
assert any(call[0] == "get_summary_threads" for call in fake_review_comment_gateway.calls)
|
|
53
|
+
assert not any(call[0] == "process_summary_reply" for call in fake_review_comment_gateway.calls)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_process_thread_reply_skips_when_no_allowed_files(
|
|
58
|
+
summary_reply_review_runner: SummaryReplyReviewRunner,
|
|
59
|
+
fake_review_policy_service: FakeReviewPolicyService,
|
|
60
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
61
|
+
):
|
|
62
|
+
"""Should skip processing thread if policy filtered out all files."""
|
|
63
|
+
fake_review_policy_service.responses["apply_for_files"] = []
|
|
64
|
+
|
|
65
|
+
review_info = ReviewInfoSchema(base_sha="A", head_sha="B", changed_files=["a.py"])
|
|
66
|
+
thread = ReviewThreadSchema(
|
|
67
|
+
id="99",
|
|
68
|
+
kind=ThreadKind.SUMMARY,
|
|
69
|
+
comments=[ReviewCommentSchema(id="c1", body="Summary comment")],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
await summary_reply_review_runner.process_thread_reply(thread, review_info)
|
|
73
|
+
|
|
74
|
+
assert not any(call[0] == "process_summary_reply" for call in fake_review_comment_gateway.calls)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_process_thread_reply_skips_when_no_reply(
|
|
79
|
+
summary_reply_review_runner: SummaryReplyReviewRunner,
|
|
80
|
+
fake_summary_comment_reply_service: FakeSummaryCommentReplyService,
|
|
81
|
+
fake_review_llm_gateway: FakeReviewLLMGateway,
|
|
82
|
+
fake_review_comment_gateway: FakeReviewCommentGateway,
|
|
83
|
+
):
|
|
84
|
+
"""Should not post reply if model output produced no reply schema."""
|
|
85
|
+
fake_summary_comment_reply_service.reply = None
|
|
86
|
+
|
|
87
|
+
review_info = ReviewInfoSchema(base_sha="A", head_sha="B", changed_files=["x.py"])
|
|
88
|
+
thread = ReviewThreadSchema(
|
|
89
|
+
id="42",
|
|
90
|
+
kind=ThreadKind.SUMMARY,
|
|
91
|
+
comments=[ReviewCommentSchema(id="cm1", body="AI summary comment")],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
await summary_reply_review_runner.process_thread_reply(thread, review_info)
|
|
95
|
+
|
|
96
|
+
assert any(call[0] == "ask" for call in fake_review_llm_gateway.calls)
|
|
97
|
+
assert not any(call[0] == "process_summary_reply" for call in fake_review_comment_gateway.calls)
|
|
@@ -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)
|