xai-review 0.20.0__py3-none-any.whl → 0.22.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/clients/claude/client.py +1 -1
- ai_review/clients/gemini/client.py +1 -1
- ai_review/clients/github/client.py +1 -1
- ai_review/clients/github/pr/client.py +64 -16
- ai_review/clients/github/pr/schema/comments.py +4 -0
- ai_review/clients/github/pr/schema/files.py +4 -0
- ai_review/clients/github/pr/schema/reviews.py +4 -0
- ai_review/clients/github/pr/types.py +49 -0
- ai_review/clients/gitlab/client.py +1 -1
- ai_review/clients/gitlab/mr/client.py +25 -8
- ai_review/clients/gitlab/mr/schema/discussions.py +4 -0
- ai_review/clients/gitlab/mr/schema/notes.py +4 -0
- ai_review/clients/gitlab/mr/types.py +35 -0
- ai_review/clients/openai/client.py +1 -1
- ai_review/config.py +2 -0
- ai_review/libs/asynchronous/gather.py +6 -3
- ai_review/libs/config/core.py +5 -0
- ai_review/libs/http/event_hooks/logger.py +5 -2
- ai_review/libs/http/transports/retry.py +23 -6
- ai_review/services/artifacts/service.py +2 -1
- ai_review/services/artifacts/types.py +20 -0
- ai_review/services/cost/service.py +2 -1
- ai_review/services/cost/types.py +12 -0
- ai_review/services/diff/service.py +2 -1
- ai_review/services/diff/types.py +28 -0
- ai_review/services/hook/__init__.py +5 -0
- ai_review/services/hook/constants.py +24 -0
- ai_review/services/hook/service.py +162 -0
- ai_review/services/hook/types.py +28 -0
- ai_review/services/llm/claude/client.py +2 -2
- ai_review/services/llm/factory.py +2 -2
- ai_review/services/llm/gemini/client.py +2 -2
- ai_review/services/llm/openai/client.py +2 -2
- ai_review/services/llm/types.py +1 -1
- ai_review/services/prompt/service.py +2 -1
- ai_review/services/prompt/types.py +27 -0
- ai_review/services/review/gateway/__init__.py +0 -0
- ai_review/services/review/gateway/comment.py +65 -0
- ai_review/services/review/gateway/llm.py +40 -0
- ai_review/services/review/inline/schema.py +2 -2
- ai_review/services/review/inline/service.py +2 -1
- ai_review/services/review/inline/types.py +11 -0
- ai_review/services/review/service.py +23 -74
- ai_review/services/review/summary/service.py +2 -1
- ai_review/services/review/summary/types.py +8 -0
- ai_review/services/vcs/factory.py +2 -2
- ai_review/services/vcs/github/client.py +4 -2
- ai_review/services/vcs/gitlab/client.py +4 -2
- ai_review/services/vcs/types.py +1 -1
- ai_review/tests/fixtures/clients/__init__.py +0 -0
- ai_review/tests/fixtures/clients/claude.py +22 -0
- ai_review/tests/fixtures/clients/gemini.py +21 -0
- ai_review/tests/fixtures/clients/github.py +181 -0
- ai_review/tests/fixtures/clients/gitlab.py +150 -0
- ai_review/tests/fixtures/clients/openai.py +21 -0
- ai_review/tests/fixtures/services/__init__.py +0 -0
- ai_review/tests/fixtures/services/artifacts.py +51 -0
- ai_review/tests/fixtures/services/cost.py +48 -0
- ai_review/tests/fixtures/services/diff.py +46 -0
- ai_review/tests/fixtures/{git.py → services/git.py} +11 -5
- ai_review/tests/fixtures/services/llm.py +26 -0
- ai_review/tests/fixtures/services/prompt.py +43 -0
- ai_review/tests/fixtures/services/review/__init__.py +0 -0
- ai_review/tests/fixtures/services/review/inline.py +25 -0
- ai_review/tests/fixtures/services/review/summary.py +19 -0
- ai_review/tests/fixtures/services/vcs.py +49 -0
- ai_review/tests/suites/clients/claude/test_client.py +1 -20
- ai_review/tests/suites/clients/gemini/test_client.py +1 -19
- ai_review/tests/suites/clients/github/test_client.py +1 -23
- ai_review/tests/suites/clients/gitlab/test_client.py +1 -22
- ai_review/tests/suites/clients/openai/test_client.py +1 -19
- ai_review/tests/suites/libs/asynchronous/__init__.py +0 -0
- ai_review/tests/suites/libs/asynchronous/test_gather.py +46 -0
- ai_review/tests/suites/services/diff/test_service.py +4 -4
- ai_review/tests/suites/services/diff/test_tools.py +10 -10
- ai_review/tests/suites/services/hook/__init__.py +0 -0
- ai_review/tests/suites/services/hook/test_service.py +93 -0
- ai_review/tests/suites/services/llm/__init__.py +0 -0
- ai_review/tests/suites/services/llm/test_factory.py +30 -0
- ai_review/tests/suites/services/review/inline/test_schema.py +10 -9
- ai_review/tests/suites/services/review/summary/test_schema.py +0 -1
- ai_review/tests/suites/services/review/summary/test_service.py +10 -7
- ai_review/tests/suites/services/review/test_service.py +126 -0
- ai_review/tests/suites/services/vcs/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/github/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/github/test_service.py +114 -0
- ai_review/tests/suites/services/vcs/gitlab/__init__.py +0 -0
- ai_review/tests/suites/services/vcs/gitlab/test_service.py +123 -0
- ai_review/tests/suites/services/vcs/test_factory.py +23 -0
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/METADATA +5 -2
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/RECORD +95 -50
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/WHEEL +0 -0
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/top_level.txt +0 -0
|
@@ -4,13 +4,16 @@ from ai_review.services.review.summary.schema import SummaryCommentSchema
|
|
|
4
4
|
from ai_review.services.review.summary.service import SummaryCommentService
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
@pytest.mark.parametrize(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
@pytest.mark.parametrize(
|
|
8
|
+
"raw, expected",
|
|
9
|
+
[
|
|
10
|
+
("Some summary", "Some summary"),
|
|
11
|
+
(" padded summary ", "padded summary"),
|
|
12
|
+
("", ""),
|
|
13
|
+
(None, ""),
|
|
14
|
+
]
|
|
15
|
+
)
|
|
16
|
+
def test_parse_model_output_normalizes_and_wraps(raw: str | None, expected: str):
|
|
14
17
|
result = SummaryCommentService.parse_model_output(raw)
|
|
15
18
|
assert isinstance(result, SummaryCommentSchema)
|
|
16
19
|
assert result.text == expected
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.review.service import ReviewService
|
|
4
|
+
from ai_review.tests.fixtures.services.artifacts import FakeArtifactsService
|
|
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.llm import FakeLLMClient
|
|
9
|
+
from ai_review.tests.fixtures.services.prompt import FakePromptService
|
|
10
|
+
from ai_review.tests.fixtures.services.review.inline import FakeInlineCommentService
|
|
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()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_run_inline_review_happy_path(
|
|
42
|
+
review_service: ReviewService,
|
|
43
|
+
fake_vcs_client: FakeVCSClient,
|
|
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,
|
|
50
|
+
):
|
|
51
|
+
"""Should perform inline review for changed files and create inline comments via VCS."""
|
|
52
|
+
fake_git_service.responses["get_diff_for_file"] = "FAKE_DIFF"
|
|
53
|
+
|
|
54
|
+
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)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_run_inline_review_skips_when_no_diff(
|
|
75
|
+
review_service: ReviewService,
|
|
76
|
+
fake_vcs_client: FakeVCSClient,
|
|
77
|
+
fake_git_service: FakeGitService,
|
|
78
|
+
fake_llm_client: FakeLLMClient,
|
|
79
|
+
):
|
|
80
|
+
"""Should skip inline review when no diffs exist."""
|
|
81
|
+
fake_git_service.responses["get_diff_for_file"] = ""
|
|
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)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_run_context_review_happy_path(
|
|
91
|
+
review_service: ReviewService,
|
|
92
|
+
fake_vcs_client: FakeVCSClient,
|
|
93
|
+
fake_llm_client: FakeLLMClient,
|
|
94
|
+
fake_prompt_service: FakePromptService,
|
|
95
|
+
fake_diff_service: FakeDiffService,
|
|
96
|
+
):
|
|
97
|
+
"""Should perform context review and post inline comments based on rendered files."""
|
|
98
|
+
await review_service.run_context_review()
|
|
99
|
+
|
|
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
|
+
|
|
104
|
+
assert any(call[0] == "render_files" for call in fake_diff_service.calls)
|
|
105
|
+
assert any(call[0] == "build_context_request" for call in fake_prompt_service.calls)
|
|
106
|
+
assert any(call[0] == "chat" for call in fake_llm_client.calls)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_run_summary_review_happy_path(
|
|
111
|
+
review_service: ReviewService,
|
|
112
|
+
fake_vcs_client: FakeVCSClient,
|
|
113
|
+
fake_llm_client: FakeLLMClient,
|
|
114
|
+
fake_prompt_service: FakePromptService,
|
|
115
|
+
fake_diff_service: FakeDiffService,
|
|
116
|
+
):
|
|
117
|
+
"""Should perform summary review and post a single summary comment."""
|
|
118
|
+
await review_service.run_summary_review()
|
|
119
|
+
|
|
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
|
+
|
|
124
|
+
assert any(call[0] == "render_files" for call in fake_diff_service.calls)
|
|
125
|
+
assert any(call[0] == "build_summary_request" for call in fake_prompt_service.calls)
|
|
126
|
+
assert any(call[0] == "chat" for call in fake_llm_client.calls)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.vcs.github.client import GitHubVCSClient
|
|
4
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema
|
|
5
|
+
from ai_review.tests.fixtures.clients.github import FakeGitHubPullRequestsHTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.asyncio
|
|
9
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
10
|
+
async def test_get_review_info_returns_valid_schema(
|
|
11
|
+
github_vcs_client: GitHubVCSClient,
|
|
12
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
13
|
+
):
|
|
14
|
+
"""Should return detailed PR info with branches, author, and files."""
|
|
15
|
+
info = await github_vcs_client.get_review_info()
|
|
16
|
+
|
|
17
|
+
assert isinstance(info, ReviewInfoSchema)
|
|
18
|
+
assert info.id == 1
|
|
19
|
+
assert info.title == "Fake Pull Request"
|
|
20
|
+
assert info.description == "This is a fake PR for testing"
|
|
21
|
+
|
|
22
|
+
assert info.author.username == "tester"
|
|
23
|
+
assert {a.username for a in info.assignees} == {"dev1", "dev2"}
|
|
24
|
+
assert {r.username for r in info.reviewers} == {"reviewer"}
|
|
25
|
+
|
|
26
|
+
assert info.source_branch.ref == "feature/test"
|
|
27
|
+
assert info.target_branch.ref == "main"
|
|
28
|
+
assert info.base_sha == "abc123"
|
|
29
|
+
assert info.head_sha == "def456"
|
|
30
|
+
|
|
31
|
+
assert "app/main.py" in info.changed_files
|
|
32
|
+
assert len(info.changed_files) == 2
|
|
33
|
+
|
|
34
|
+
called_methods = [name for name, _ in fake_github_pull_requests_http_client.calls]
|
|
35
|
+
assert called_methods == ["get_pull_request", "get_files"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.asyncio
|
|
39
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
40
|
+
async def test_get_general_comments_returns_expected_list(
|
|
41
|
+
github_vcs_client: GitHubVCSClient,
|
|
42
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
43
|
+
):
|
|
44
|
+
"""Should return general (issue-level) comments."""
|
|
45
|
+
comments = await github_vcs_client.get_general_comments()
|
|
46
|
+
|
|
47
|
+
assert all(isinstance(c, ReviewCommentSchema) for c in comments)
|
|
48
|
+
assert len(comments) == 2
|
|
49
|
+
|
|
50
|
+
bodies = [c.body for c in comments]
|
|
51
|
+
assert "General comment" in bodies
|
|
52
|
+
assert "Another general comment" in bodies
|
|
53
|
+
|
|
54
|
+
called_methods = [name for name, _ in fake_github_pull_requests_http_client.calls]
|
|
55
|
+
assert called_methods == ["get_issue_comments"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
60
|
+
async def test_get_inline_comments_returns_expected_list(
|
|
61
|
+
github_vcs_client: GitHubVCSClient,
|
|
62
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
63
|
+
):
|
|
64
|
+
"""Should return inline comments with file and line references."""
|
|
65
|
+
comments = await github_vcs_client.get_inline_comments()
|
|
66
|
+
|
|
67
|
+
assert all(isinstance(c, ReviewCommentSchema) for c in comments)
|
|
68
|
+
assert len(comments) == 2
|
|
69
|
+
|
|
70
|
+
first = comments[0]
|
|
71
|
+
assert first.body == "Inline comment"
|
|
72
|
+
assert first.file == "file.py"
|
|
73
|
+
assert first.line == 5
|
|
74
|
+
|
|
75
|
+
called_methods = [name for name, _ in fake_github_pull_requests_http_client.calls]
|
|
76
|
+
assert called_methods == ["get_review_comments"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
81
|
+
async def test_create_general_comment_posts_comment(
|
|
82
|
+
github_vcs_client: GitHubVCSClient,
|
|
83
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
84
|
+
):
|
|
85
|
+
"""Should post a general (non-inline) comment."""
|
|
86
|
+
message = "Hello from test!"
|
|
87
|
+
|
|
88
|
+
await github_vcs_client.create_general_comment(message)
|
|
89
|
+
|
|
90
|
+
calls = [args for name, args in fake_github_pull_requests_http_client.calls if name == "create_issue_comment"]
|
|
91
|
+
assert len(calls) == 1
|
|
92
|
+
call_args = calls[0]
|
|
93
|
+
assert call_args["body"] == message
|
|
94
|
+
assert call_args["repo"] == "repo"
|
|
95
|
+
assert call_args["owner"] == "owner"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
100
|
+
async def test_create_inline_comment_posts_comment(
|
|
101
|
+
github_vcs_client: GitHubVCSClient,
|
|
102
|
+
fake_github_pull_requests_http_client: FakeGitHubPullRequestsHTTPClient,
|
|
103
|
+
):
|
|
104
|
+
"""Should post an inline comment with correct path, line and commit_id."""
|
|
105
|
+
await github_vcs_client.create_inline_comment("file.py", 10, "Looks good")
|
|
106
|
+
|
|
107
|
+
calls = [args for name, args in fake_github_pull_requests_http_client.calls if name == "create_review_comment"]
|
|
108
|
+
assert len(calls) == 1
|
|
109
|
+
|
|
110
|
+
call_args = calls[0]
|
|
111
|
+
assert call_args["path"] == "file.py"
|
|
112
|
+
assert call_args["line"] == 10
|
|
113
|
+
assert call_args["body"] == "Looks good"
|
|
114
|
+
assert call_args["commit_id"] == "def456" # from fake PR head
|
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.vcs.gitlab.client import GitLabVCSClient
|
|
4
|
+
from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema
|
|
5
|
+
from ai_review.tests.fixtures.clients.gitlab import FakeGitLabMergeRequestsHTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.asyncio
|
|
9
|
+
@pytest.mark.usefixtures("gitlab_http_client_config")
|
|
10
|
+
async def test_get_review_info_returns_valid_schema(
|
|
11
|
+
gitlab_vcs_client: GitLabVCSClient,
|
|
12
|
+
fake_gitlab_merge_requests_http_client: FakeGitLabMergeRequestsHTTPClient,
|
|
13
|
+
):
|
|
14
|
+
"""Should return valid MR info with author, branches and changed files."""
|
|
15
|
+
info = await gitlab_vcs_client.get_review_info()
|
|
16
|
+
|
|
17
|
+
assert isinstance(info, ReviewInfoSchema)
|
|
18
|
+
assert info.id == 1
|
|
19
|
+
assert info.title == "Fake Merge Request"
|
|
20
|
+
assert info.description == "This is a fake MR for testing"
|
|
21
|
+
|
|
22
|
+
assert info.author.username == "tester"
|
|
23
|
+
assert info.author.name == "Tester"
|
|
24
|
+
assert info.author.id == 42
|
|
25
|
+
|
|
26
|
+
assert info.source_branch.ref == "feature/test"
|
|
27
|
+
assert info.target_branch.ref == "main"
|
|
28
|
+
assert info.base_sha == "abc123"
|
|
29
|
+
assert info.head_sha == "def456"
|
|
30
|
+
assert info.start_sha == "ghi789"
|
|
31
|
+
|
|
32
|
+
assert len(info.changed_files) == 1
|
|
33
|
+
assert info.changed_files[0] == "main.py"
|
|
34
|
+
|
|
35
|
+
called_methods = [name for name, _ in fake_gitlab_merge_requests_http_client.calls]
|
|
36
|
+
assert called_methods == ["get_changes"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
@pytest.mark.usefixtures("gitlab_http_client_config")
|
|
41
|
+
async def test_get_general_comments_returns_expected_list(
|
|
42
|
+
gitlab_vcs_client: GitLabVCSClient,
|
|
43
|
+
fake_gitlab_merge_requests_http_client: FakeGitLabMergeRequestsHTTPClient,
|
|
44
|
+
):
|
|
45
|
+
"""Should return general MR-level notes."""
|
|
46
|
+
comments = await gitlab_vcs_client.get_general_comments()
|
|
47
|
+
|
|
48
|
+
assert all(isinstance(c, ReviewCommentSchema) for c in comments)
|
|
49
|
+
assert len(comments) == 2
|
|
50
|
+
|
|
51
|
+
bodies = [c.body for c in comments]
|
|
52
|
+
assert "General comment" in bodies
|
|
53
|
+
assert "Another note" in bodies
|
|
54
|
+
|
|
55
|
+
called_methods = [name for name, _ in fake_gitlab_merge_requests_http_client.calls]
|
|
56
|
+
assert called_methods == ["get_notes"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
@pytest.mark.usefixtures("gitlab_http_client_config")
|
|
61
|
+
async def test_get_inline_comments_returns_expected_list(
|
|
62
|
+
gitlab_vcs_client: GitLabVCSClient,
|
|
63
|
+
fake_gitlab_merge_requests_http_client: FakeGitLabMergeRequestsHTTPClient,
|
|
64
|
+
):
|
|
65
|
+
"""Should return inline comments from MR discussions."""
|
|
66
|
+
comments = await gitlab_vcs_client.get_inline_comments()
|
|
67
|
+
|
|
68
|
+
assert all(isinstance(c, ReviewCommentSchema) for c in comments)
|
|
69
|
+
assert len(comments) == 2
|
|
70
|
+
|
|
71
|
+
first = comments[0]
|
|
72
|
+
assert first.body == "Inline comment A"
|
|
73
|
+
|
|
74
|
+
called_methods = [name for name, _ in fake_gitlab_merge_requests_http_client.calls]
|
|
75
|
+
assert called_methods == ["get_discussions"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
@pytest.mark.usefixtures("gitlab_http_client_config")
|
|
80
|
+
async def test_create_general_comment_posts_comment(
|
|
81
|
+
gitlab_vcs_client: GitLabVCSClient,
|
|
82
|
+
fake_gitlab_merge_requests_http_client: FakeGitLabMergeRequestsHTTPClient,
|
|
83
|
+
):
|
|
84
|
+
"""Should post a general note to MR."""
|
|
85
|
+
message = "Hello, GitLab!"
|
|
86
|
+
|
|
87
|
+
await gitlab_vcs_client.create_general_comment(message)
|
|
88
|
+
|
|
89
|
+
calls = [
|
|
90
|
+
args for name, args in fake_gitlab_merge_requests_http_client.calls
|
|
91
|
+
if name == "create_note"
|
|
92
|
+
]
|
|
93
|
+
assert len(calls) == 1
|
|
94
|
+
call_args = calls[0]
|
|
95
|
+
|
|
96
|
+
assert call_args["body"] == message
|
|
97
|
+
assert call_args["project_id"] == "project-id"
|
|
98
|
+
assert call_args["merge_request_id"] == "merge-request-id"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.asyncio
|
|
102
|
+
@pytest.mark.usefixtures("gitlab_http_client_config")
|
|
103
|
+
async def test_create_inline_comment_posts_comment(
|
|
104
|
+
gitlab_vcs_client: GitLabVCSClient,
|
|
105
|
+
fake_gitlab_merge_requests_http_client: FakeGitLabMergeRequestsHTTPClient,
|
|
106
|
+
):
|
|
107
|
+
"""Should create an inline discussion at specific file and line."""
|
|
108
|
+
await gitlab_vcs_client.create_inline_comment("main.py", 5, "Looks good!")
|
|
109
|
+
|
|
110
|
+
called_names = [name for name, _ in fake_gitlab_merge_requests_http_client.calls]
|
|
111
|
+
assert "get_changes" in called_names
|
|
112
|
+
assert "create_discussion" in called_names
|
|
113
|
+
|
|
114
|
+
calls = [
|
|
115
|
+
args for name, args in fake_gitlab_merge_requests_http_client.calls
|
|
116
|
+
if name == "create_discussion"
|
|
117
|
+
]
|
|
118
|
+
assert len(calls) == 1
|
|
119
|
+
|
|
120
|
+
call_args = calls[0]
|
|
121
|
+
assert call_args["body"] == "Looks good!"
|
|
122
|
+
assert call_args["project_id"] == "project-id"
|
|
123
|
+
assert call_args["merge_request_id"] == "merge-request-id"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.vcs.factory import get_vcs_client
|
|
4
|
+
from ai_review.services.vcs.github.client import GitHubVCSClient
|
|
5
|
+
from ai_review.services.vcs.gitlab.client import GitLabVCSClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.usefixtures("github_http_client_config")
|
|
9
|
+
def test_get_vcs_client_returns_github(monkeypatch: pytest.MonkeyPatch):
|
|
10
|
+
client = get_vcs_client()
|
|
11
|
+
assert isinstance(client, GitHubVCSClient)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.usefixtures("gitlab_http_client_config")
|
|
15
|
+
def test_get_vcs_client_returns_gitlab(monkeypatch: pytest.MonkeyPatch):
|
|
16
|
+
client = get_vcs_client()
|
|
17
|
+
assert isinstance(client, GitLabVCSClient)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_get_vcs_client_unsupported_provider(monkeypatch: pytest.MonkeyPatch):
|
|
21
|
+
monkeypatch.setattr("ai_review.services.vcs.factory.settings.vcs.provider", "BITBUCKET")
|
|
22
|
+
with pytest.raises(ValueError):
|
|
23
|
+
get_vcs_client()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xai-review
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.22.0
|
|
4
4
|
Summary: AI-powered code review tool
|
|
5
5
|
Author-email: Nikita Filonov <nikita.filonov@example.com>
|
|
6
6
|
Maintainer-email: Nikita Filonov <nikita.filonov@example.com>
|
|
@@ -29,6 +29,7 @@ Requires-Dist: pydantic
|
|
|
29
29
|
Requires-Dist: pydantic-settings
|
|
30
30
|
Provides-Extra: test
|
|
31
31
|
Requires-Dist: pytest; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
32
33
|
Dynamic: license-file
|
|
33
34
|
|
|
34
35
|
# AI Review
|
|
@@ -192,6 +193,7 @@ Add a workflow like this (manual trigger from **Actions** tab):
|
|
|
192
193
|
|
|
193
194
|
```yaml
|
|
194
195
|
name: AI Review
|
|
196
|
+
|
|
195
197
|
on:
|
|
196
198
|
workflow_dispatch:
|
|
197
199
|
inputs:
|
|
@@ -207,7 +209,7 @@ jobs:
|
|
|
207
209
|
runs-on: ubuntu-latest
|
|
208
210
|
steps:
|
|
209
211
|
- uses: actions/checkout@v4
|
|
210
|
-
- uses: Nikita-Filonov/ai-review@v0.
|
|
212
|
+
- uses: Nikita-Filonov/ai-review@v0.22.0
|
|
211
213
|
with:
|
|
212
214
|
review-command: ${{ inputs.review-command }}
|
|
213
215
|
env:
|
|
@@ -272,6 +274,7 @@ ai-review:
|
|
|
272
274
|
See these folders for reference templates and full configuration options:
|
|
273
275
|
|
|
274
276
|
- [./docs/ci](./docs/ci) — CI/CD integration templates (GitHub Actions, GitLab CI)
|
|
277
|
+
- [./docs/hooks](./docs/hooks) — hook reference and lifecycle events
|
|
275
278
|
- [./docs/configs](./docs/configs) — full configuration examples (`.yaml`, `.json`, `.env`)
|
|
276
279
|
- [./docs/prompts](./docs/prompts) — prompt templates for Python/Go (light & strict modes)
|
|
277
280
|
|