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.

Files changed (95) hide show
  1. ai_review/clients/claude/client.py +1 -1
  2. ai_review/clients/gemini/client.py +1 -1
  3. ai_review/clients/github/client.py +1 -1
  4. ai_review/clients/github/pr/client.py +64 -16
  5. ai_review/clients/github/pr/schema/comments.py +4 -0
  6. ai_review/clients/github/pr/schema/files.py +4 -0
  7. ai_review/clients/github/pr/schema/reviews.py +4 -0
  8. ai_review/clients/github/pr/types.py +49 -0
  9. ai_review/clients/gitlab/client.py +1 -1
  10. ai_review/clients/gitlab/mr/client.py +25 -8
  11. ai_review/clients/gitlab/mr/schema/discussions.py +4 -0
  12. ai_review/clients/gitlab/mr/schema/notes.py +4 -0
  13. ai_review/clients/gitlab/mr/types.py +35 -0
  14. ai_review/clients/openai/client.py +1 -1
  15. ai_review/config.py +2 -0
  16. ai_review/libs/asynchronous/gather.py +6 -3
  17. ai_review/libs/config/core.py +5 -0
  18. ai_review/libs/http/event_hooks/logger.py +5 -2
  19. ai_review/libs/http/transports/retry.py +23 -6
  20. ai_review/services/artifacts/service.py +2 -1
  21. ai_review/services/artifacts/types.py +20 -0
  22. ai_review/services/cost/service.py +2 -1
  23. ai_review/services/cost/types.py +12 -0
  24. ai_review/services/diff/service.py +2 -1
  25. ai_review/services/diff/types.py +28 -0
  26. ai_review/services/hook/__init__.py +5 -0
  27. ai_review/services/hook/constants.py +24 -0
  28. ai_review/services/hook/service.py +162 -0
  29. ai_review/services/hook/types.py +28 -0
  30. ai_review/services/llm/claude/client.py +2 -2
  31. ai_review/services/llm/factory.py +2 -2
  32. ai_review/services/llm/gemini/client.py +2 -2
  33. ai_review/services/llm/openai/client.py +2 -2
  34. ai_review/services/llm/types.py +1 -1
  35. ai_review/services/prompt/service.py +2 -1
  36. ai_review/services/prompt/types.py +27 -0
  37. ai_review/services/review/gateway/__init__.py +0 -0
  38. ai_review/services/review/gateway/comment.py +65 -0
  39. ai_review/services/review/gateway/llm.py +40 -0
  40. ai_review/services/review/inline/schema.py +2 -2
  41. ai_review/services/review/inline/service.py +2 -1
  42. ai_review/services/review/inline/types.py +11 -0
  43. ai_review/services/review/service.py +23 -74
  44. ai_review/services/review/summary/service.py +2 -1
  45. ai_review/services/review/summary/types.py +8 -0
  46. ai_review/services/vcs/factory.py +2 -2
  47. ai_review/services/vcs/github/client.py +4 -2
  48. ai_review/services/vcs/gitlab/client.py +4 -2
  49. ai_review/services/vcs/types.py +1 -1
  50. ai_review/tests/fixtures/clients/__init__.py +0 -0
  51. ai_review/tests/fixtures/clients/claude.py +22 -0
  52. ai_review/tests/fixtures/clients/gemini.py +21 -0
  53. ai_review/tests/fixtures/clients/github.py +181 -0
  54. ai_review/tests/fixtures/clients/gitlab.py +150 -0
  55. ai_review/tests/fixtures/clients/openai.py +21 -0
  56. ai_review/tests/fixtures/services/__init__.py +0 -0
  57. ai_review/tests/fixtures/services/artifacts.py +51 -0
  58. ai_review/tests/fixtures/services/cost.py +48 -0
  59. ai_review/tests/fixtures/services/diff.py +46 -0
  60. ai_review/tests/fixtures/{git.py → services/git.py} +11 -5
  61. ai_review/tests/fixtures/services/llm.py +26 -0
  62. ai_review/tests/fixtures/services/prompt.py +43 -0
  63. ai_review/tests/fixtures/services/review/__init__.py +0 -0
  64. ai_review/tests/fixtures/services/review/inline.py +25 -0
  65. ai_review/tests/fixtures/services/review/summary.py +19 -0
  66. ai_review/tests/fixtures/services/vcs.py +49 -0
  67. ai_review/tests/suites/clients/claude/test_client.py +1 -20
  68. ai_review/tests/suites/clients/gemini/test_client.py +1 -19
  69. ai_review/tests/suites/clients/github/test_client.py +1 -23
  70. ai_review/tests/suites/clients/gitlab/test_client.py +1 -22
  71. ai_review/tests/suites/clients/openai/test_client.py +1 -19
  72. ai_review/tests/suites/libs/asynchronous/__init__.py +0 -0
  73. ai_review/tests/suites/libs/asynchronous/test_gather.py +46 -0
  74. ai_review/tests/suites/services/diff/test_service.py +4 -4
  75. ai_review/tests/suites/services/diff/test_tools.py +10 -10
  76. ai_review/tests/suites/services/hook/__init__.py +0 -0
  77. ai_review/tests/suites/services/hook/test_service.py +93 -0
  78. ai_review/tests/suites/services/llm/__init__.py +0 -0
  79. ai_review/tests/suites/services/llm/test_factory.py +30 -0
  80. ai_review/tests/suites/services/review/inline/test_schema.py +10 -9
  81. ai_review/tests/suites/services/review/summary/test_schema.py +0 -1
  82. ai_review/tests/suites/services/review/summary/test_service.py +10 -7
  83. ai_review/tests/suites/services/review/test_service.py +126 -0
  84. ai_review/tests/suites/services/vcs/__init__.py +0 -0
  85. ai_review/tests/suites/services/vcs/github/__init__.py +0 -0
  86. ai_review/tests/suites/services/vcs/github/test_service.py +114 -0
  87. ai_review/tests/suites/services/vcs/gitlab/__init__.py +0 -0
  88. ai_review/tests/suites/services/vcs/gitlab/test_service.py +123 -0
  89. ai_review/tests/suites/services/vcs/test_factory.py +23 -0
  90. {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/METADATA +5 -2
  91. {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/RECORD +95 -50
  92. {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/WHEEL +0 -0
  93. {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/entry_points.txt +0 -0
  94. {xai_review-0.20.0.dist-info → xai_review-0.22.0.dist-info}/licenses/LICENSE +0 -0
  95. {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("raw, expected", [
8
- ("Some summary", "Some summary"),
9
- (" padded summary ", "padded summary"),
10
- ("", ""),
11
- (None, ""),
12
- ])
13
- def test_parse_model_output_normalizes_and_wraps(raw, expected):
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.20.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.20.0
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