claude-dev-env 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -0
- package/_shared/pr-loop/audit-contract.md +4 -3
- package/_shared/pr-loop/fix-protocol.md +2 -0
- package/_shared/pr-loop/gh-payloads.md +38 -37
- package/_shared/pr-loop/scripts/README.md +0 -1
- package/_shared/pr-loop/scripts/preflight.py +2 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +2 -2
- package/_shared/pr-loop/scripts/tests/test_preflight.py +22 -0
- package/_shared/pr-loop/state-schema.md +10 -10
- package/agents/clean-coder.md +4 -0
- package/agents/code-quality-agent.md +23 -85
- package/agents/groq-coder.md +8 -6
- package/hooks/blocking/__init__.py +0 -0
- package/hooks/blocking/hedging_language_blocker.py +2 -2
- package/hooks/blocking/state_description_blocker.py +243 -0
- package/hooks/blocking/tdd_enforcer.py +94 -0
- package/hooks/blocking/test_hedging_language_blocker.py +1 -1
- package/hooks/blocking/test_state_description_blocker.py +618 -0
- package/hooks/blocking/test_tdd_enforcer.py +152 -0
- package/hooks/config/state_description_blocker_constants.py +130 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +4 -50
- package/rules/no-historical-clutter.md +57 -0
- package/scripts/config/groq_bugteam_config.py +13 -5
- package/skills/bugteam/CONSTRAINTS.md +20 -27
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/PROMPTS.md +78 -42
- package/skills/bugteam/SKILL.md +76 -63
- package/skills/bugteam/SKILL_EVALS.md +12 -12
- package/skills/bugteam/reference/audit-and-teammates.md +21 -48
- package/skills/bugteam/reference/audit-contract.md +7 -7
- package/skills/bugteam/reference/github-pr-reviews.md +31 -31
- package/skills/bugteam/reference/team-setup.md +1 -1
- package/skills/bugteam/reference/teardown-publish-permissions.md +4 -4
- package/skills/copilot-review/SKILL.md +7 -14
- package/skills/findbugs/SKILL.md +2 -2
- package/skills/fixbugs/SKILL.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -6
- package/skills/pr-converge/SKILL.md +7 -6
- package/skills/pr-converge/reference/convergence-gates.md +46 -44
- package/skills/pr-converge/reference/examples.md +4 -4
- package/skills/pr-converge/reference/fix-protocol.md +8 -8
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -10
- package/skills/pr-converge/reference/per-tick.md +24 -36
- package/skills/pr-converge/reference/stop-conditions.md +7 -7
- package/skills/pr-converge/scripts/README.md +65 -117
- package/skills/pr-review-responder/EXAMPLES.md +2 -2
- package/skills/pr-review-responder/PRINCIPLES.md +2 -8
- package/skills/pr-review-responder/README.md +7 -48
- package/skills/pr-review-responder/SKILL.md +2 -3
- package/skills/pr-review-responder/TESTING.md +8 -65
- package/skills/qbug/SKILL.md +10 -16
- package/_shared/pr-loop/scripts/config/gh_util_constants.py +0 -31
- package/_shared/pr-loop/scripts/gh_util.py +0 -193
- package/_shared/pr-loop/scripts/tests/test_gh_util.py +0 -257
- package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +0 -61
- package/skills/pr-converge/scripts/check_pr_mergeability.py +0 -78
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +0 -118
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -152
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +0 -70
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +0 -57
- package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +0 -70
- package/skills/pr-converge/scripts/fetch_claude_reviews.py +0 -61
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +0 -70
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +0 -61
- package/skills/pr-converge/scripts/mark_pr_ready.py +0 -54
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +0 -49
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +0 -33
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +0 -84
- package/skills/pr-converge/scripts/request_copilot_review.py +0 -71
- package/skills/pr-converge/scripts/resolve_pr_head.py +0 -58
- package/skills/pr-converge/scripts/review_field_helpers.py +0 -43
- package/skills/pr-converge/scripts/reviewer_fetch_core.py +0 -153
- package/skills/pr-converge/scripts/reviewer_specs.py +0 -98
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +0 -126
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +0 -443
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +0 -299
- package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +0 -485
- package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +0 -368
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +0 -440
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +0 -366
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +0 -69
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +0 -195
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +0 -159
- package/skills/pr-converge/scripts/test_request_copilot_review.py +0 -101
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +0 -79
- package/skills/pr-converge/scripts/test_review_field_helpers.py +0 -80
- package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +0 -448
- package/skills/pr-converge/scripts/test_reviewer_specs.py +0 -107
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +0 -139
- package/skills/pr-converge/scripts/test_view_pr_context.py +0 -111
- package/skills/pr-converge/scripts/trigger_bugbot.py +0 -77
- package/skills/pr-converge/scripts/view_pr_context.py +0 -47
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +0 -376
|
@@ -1,448 +0,0 @@
|
|
|
1
|
-
"""Tests for reviewer_fetch_core.
|
|
2
|
-
|
|
3
|
-
Covers:
|
|
4
|
-
- fetch_reviewer_reviews invokes gh against the reviews endpoint with --paginate --slurp
|
|
5
|
-
- login filter applies case-insensitively as a substring on user.login
|
|
6
|
-
- entries missing submitted_at or id are filtered out
|
|
7
|
-
- reviews are sorted newest-first by submitted_at
|
|
8
|
-
- the spec.classify_review callable is invoked for each surviving review
|
|
9
|
-
- subprocess errors propagate
|
|
10
|
-
- fetch_reviewer_inline_comments returns empty when no review for current_head
|
|
11
|
-
- fetch_reviewer_inline_comments only returns comments anchored to the latest review
|
|
12
|
-
- fetch_reviewer_inline_comments invokes gh against the comments endpoint with --paginate --slurp
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import importlib.util
|
|
18
|
-
import json
|
|
19
|
-
import subprocess
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
from types import ModuleType
|
|
22
|
-
from unittest.mock import MagicMock, patch
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _load_module() -> ModuleType:
|
|
26
|
-
module_path = Path(__file__).parent / "reviewer_fetch_core.py"
|
|
27
|
-
spec = importlib.util.spec_from_file_location("reviewer_fetch_core", module_path)
|
|
28
|
-
assert spec is not None
|
|
29
|
-
assert spec.loader is not None
|
|
30
|
-
module = importlib.util.module_from_spec(spec)
|
|
31
|
-
spec.loader.exec_module(module)
|
|
32
|
-
return module
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
reviewer_fetch_core_module = _load_module()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _completed(stdout: str) -> subprocess.CompletedProcess:
|
|
39
|
-
process = MagicMock(spec=subprocess.CompletedProcess)
|
|
40
|
-
process.stdout = stdout
|
|
41
|
-
process.returncode = 0
|
|
42
|
-
return process
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _fake_spec(*, login_filter_substring: str = "test") -> object:
|
|
46
|
-
fake_spec_object = MagicMock()
|
|
47
|
-
fake_spec_object.login_filter_substring = login_filter_substring
|
|
48
|
-
fake_spec_object.classify_review = MagicMock(return_value="clean")
|
|
49
|
-
return fake_spec_object
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_fetch_reviewer_reviews_invokes_gh_with_paginate_slurp_against_reviews_endpoint() -> (
|
|
53
|
-
None
|
|
54
|
-
):
|
|
55
|
-
pages_payload = json.dumps([[]])
|
|
56
|
-
with patch("subprocess.run") as mock_run:
|
|
57
|
-
mock_run.return_value = _completed(pages_payload)
|
|
58
|
-
reviewer_fetch_core_module.fetch_reviewer_reviews(
|
|
59
|
-
_fake_spec(), owner="acme", repo="widget", number=42
|
|
60
|
-
)
|
|
61
|
-
invoked_argv = mock_run.call_args[0][0]
|
|
62
|
-
assert invoked_argv[0] == "gh"
|
|
63
|
-
assert invoked_argv[1] == "api"
|
|
64
|
-
assert "repos/acme/widget/pulls/42/reviews?per_page=100" in invoked_argv[2]
|
|
65
|
-
assert "--paginate" in invoked_argv
|
|
66
|
-
assert "--slurp" in invoked_argv
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_fetch_reviewer_reviews_filters_by_login_filter_substring_case_insensitively() -> (
|
|
70
|
-
None
|
|
71
|
-
):
|
|
72
|
-
pages_payload = json.dumps(
|
|
73
|
-
[
|
|
74
|
-
[
|
|
75
|
-
{
|
|
76
|
-
"id": 1,
|
|
77
|
-
"user": {"login": "TestBot[bot]"},
|
|
78
|
-
"state": "APPROVED",
|
|
79
|
-
"commit_id": "abc",
|
|
80
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
81
|
-
"body": "uppercase login",
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
"id": 2,
|
|
85
|
-
"user": {"login": "other-reviewer"},
|
|
86
|
-
"state": "APPROVED",
|
|
87
|
-
"commit_id": "abc",
|
|
88
|
-
"submitted_at": "2026-01-02T00:00:00Z",
|
|
89
|
-
"body": "no match",
|
|
90
|
-
},
|
|
91
|
-
]
|
|
92
|
-
]
|
|
93
|
-
)
|
|
94
|
-
with patch("subprocess.run") as mock_run:
|
|
95
|
-
mock_run.return_value = _completed(pages_payload)
|
|
96
|
-
all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
|
|
97
|
-
_fake_spec(login_filter_substring="test"),
|
|
98
|
-
owner="acme",
|
|
99
|
-
repo="widget",
|
|
100
|
-
number=42,
|
|
101
|
-
)
|
|
102
|
-
assert len(all_reviews) == 1
|
|
103
|
-
assert all_reviews[0]["review_id"] == 1
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_fetch_reviewer_reviews_drops_entries_missing_submitted_at() -> None:
|
|
107
|
-
pages_payload = json.dumps(
|
|
108
|
-
[
|
|
109
|
-
[
|
|
110
|
-
{
|
|
111
|
-
"id": 1,
|
|
112
|
-
"user": {"login": "test[bot]"},
|
|
113
|
-
"state": "APPROVED",
|
|
114
|
-
"commit_id": "abc",
|
|
115
|
-
"body": "no submitted_at",
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
"id": 2,
|
|
119
|
-
"user": {"login": "test[bot]"},
|
|
120
|
-
"state": "APPROVED",
|
|
121
|
-
"commit_id": "abc",
|
|
122
|
-
"submitted_at": "2026-01-02T00:00:00Z",
|
|
123
|
-
"body": "valid",
|
|
124
|
-
},
|
|
125
|
-
]
|
|
126
|
-
]
|
|
127
|
-
)
|
|
128
|
-
with patch("subprocess.run") as mock_run:
|
|
129
|
-
mock_run.return_value = _completed(pages_payload)
|
|
130
|
-
all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
|
|
131
|
-
_fake_spec(), owner="acme", repo="widget", number=42
|
|
132
|
-
)
|
|
133
|
-
assert [each_review["review_id"] for each_review in all_reviews] == [2]
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def test_fetch_reviewer_reviews_drops_entries_missing_id() -> None:
|
|
137
|
-
pages_payload = json.dumps(
|
|
138
|
-
[
|
|
139
|
-
[
|
|
140
|
-
{
|
|
141
|
-
"user": {"login": "test[bot]"},
|
|
142
|
-
"state": "APPROVED",
|
|
143
|
-
"commit_id": "abc",
|
|
144
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
145
|
-
"body": "no id",
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
"id": 99,
|
|
149
|
-
"user": {"login": "test[bot]"},
|
|
150
|
-
"state": "APPROVED",
|
|
151
|
-
"commit_id": "abc",
|
|
152
|
-
"submitted_at": "2026-01-02T00:00:00Z",
|
|
153
|
-
"body": "valid",
|
|
154
|
-
},
|
|
155
|
-
]
|
|
156
|
-
]
|
|
157
|
-
)
|
|
158
|
-
with patch("subprocess.run") as mock_run:
|
|
159
|
-
mock_run.return_value = _completed(pages_payload)
|
|
160
|
-
all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
|
|
161
|
-
_fake_spec(), owner="acme", repo="widget", number=42
|
|
162
|
-
)
|
|
163
|
-
assert [each_review["review_id"] for each_review in all_reviews] == [99]
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def test_fetch_reviewer_reviews_sorts_newest_first_across_pages() -> None:
|
|
167
|
-
pages_payload = json.dumps(
|
|
168
|
-
[
|
|
169
|
-
[
|
|
170
|
-
{
|
|
171
|
-
"id": 10,
|
|
172
|
-
"user": {"login": "test[bot]"},
|
|
173
|
-
"state": "APPROVED",
|
|
174
|
-
"commit_id": "old",
|
|
175
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
176
|
-
"body": "oldest",
|
|
177
|
-
}
|
|
178
|
-
],
|
|
179
|
-
[
|
|
180
|
-
{
|
|
181
|
-
"id": 11,
|
|
182
|
-
"user": {"login": "test[bot]"},
|
|
183
|
-
"state": "CHANGES_REQUESTED",
|
|
184
|
-
"commit_id": "new",
|
|
185
|
-
"submitted_at": "2026-01-03T00:00:00Z",
|
|
186
|
-
"body": "newest",
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
"id": 12,
|
|
190
|
-
"user": {"login": "test[bot]"},
|
|
191
|
-
"state": "APPROVED",
|
|
192
|
-
"commit_id": "mid",
|
|
193
|
-
"submitted_at": "2026-01-02T00:00:00Z",
|
|
194
|
-
"body": "middle",
|
|
195
|
-
},
|
|
196
|
-
],
|
|
197
|
-
]
|
|
198
|
-
)
|
|
199
|
-
with patch("subprocess.run") as mock_run:
|
|
200
|
-
mock_run.return_value = _completed(pages_payload)
|
|
201
|
-
all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
|
|
202
|
-
_fake_spec(), owner="acme", repo="widget", number=42
|
|
203
|
-
)
|
|
204
|
-
assert [each_review["submitted_at"] for each_review in all_reviews] == [
|
|
205
|
-
"2026-01-03T00:00:00Z",
|
|
206
|
-
"2026-01-02T00:00:00Z",
|
|
207
|
-
"2026-01-01T00:00:00Z",
|
|
208
|
-
]
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def test_fetch_reviewer_reviews_invokes_classify_callable_per_review() -> None:
|
|
212
|
-
pages_payload = json.dumps(
|
|
213
|
-
[
|
|
214
|
-
[
|
|
215
|
-
{
|
|
216
|
-
"id": 1,
|
|
217
|
-
"user": {"login": "test[bot]"},
|
|
218
|
-
"state": "APPROVED",
|
|
219
|
-
"commit_id": "abc",
|
|
220
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
221
|
-
"body": "first",
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
"id": 2,
|
|
225
|
-
"user": {"login": "test[bot]"},
|
|
226
|
-
"state": "CHANGES_REQUESTED",
|
|
227
|
-
"commit_id": "abc",
|
|
228
|
-
"submitted_at": "2026-01-02T00:00:00Z",
|
|
229
|
-
"body": "second",
|
|
230
|
-
},
|
|
231
|
-
]
|
|
232
|
-
]
|
|
233
|
-
)
|
|
234
|
-
classify_callable = MagicMock(side_effect=["dirty", "clean"])
|
|
235
|
-
fake_spec_object = MagicMock()
|
|
236
|
-
fake_spec_object.login_filter_substring = "test"
|
|
237
|
-
fake_spec_object.classify_review = classify_callable
|
|
238
|
-
with patch("subprocess.run") as mock_run:
|
|
239
|
-
mock_run.return_value = _completed(pages_payload)
|
|
240
|
-
all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
|
|
241
|
-
fake_spec_object, owner="acme", repo="widget", number=42
|
|
242
|
-
)
|
|
243
|
-
assert classify_callable.call_count == 2
|
|
244
|
-
assert {each_review["classification"] for each_review in all_reviews} == {
|
|
245
|
-
"dirty",
|
|
246
|
-
"clean",
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def test_fetch_reviewer_reviews_propagates_subprocess_errors() -> None:
|
|
251
|
-
failure = subprocess.CalledProcessError(
|
|
252
|
-
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
253
|
-
)
|
|
254
|
-
with patch("subprocess.run", side_effect=failure):
|
|
255
|
-
try:
|
|
256
|
-
reviewer_fetch_core_module.fetch_reviewer_reviews(
|
|
257
|
-
_fake_spec(), owner="acme", repo="widget", number=42
|
|
258
|
-
)
|
|
259
|
-
assert False, "expected CalledProcessError"
|
|
260
|
-
except subprocess.CalledProcessError:
|
|
261
|
-
pass
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def test_fetch_reviewer_inline_comments_returns_empty_when_no_review_for_head() -> None:
|
|
265
|
-
no_matching_review = [
|
|
266
|
-
{
|
|
267
|
-
"review_id": 1,
|
|
268
|
-
"commit_id": "other_sha",
|
|
269
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
270
|
-
"state": "APPROVED",
|
|
271
|
-
"body": "",
|
|
272
|
-
"classification": "clean",
|
|
273
|
-
}
|
|
274
|
-
]
|
|
275
|
-
with patch("subprocess.run") as mock_run:
|
|
276
|
-
all_inline_comments = reviewer_fetch_core_module.fetch_reviewer_inline_comments(
|
|
277
|
-
_fake_spec(),
|
|
278
|
-
owner="acme",
|
|
279
|
-
repo="widget",
|
|
280
|
-
number=42,
|
|
281
|
-
current_head="missing_sha",
|
|
282
|
-
all_reviews=no_matching_review,
|
|
283
|
-
)
|
|
284
|
-
assert all_inline_comments == []
|
|
285
|
-
mock_run.assert_not_called()
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def test_fetch_reviewer_inline_comments_invokes_gh_against_comments_endpoint() -> None:
|
|
289
|
-
pages_payload = json.dumps([[]])
|
|
290
|
-
matching_review = [
|
|
291
|
-
{
|
|
292
|
-
"review_id": 1,
|
|
293
|
-
"commit_id": "abc123",
|
|
294
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
295
|
-
"state": "APPROVED",
|
|
296
|
-
"body": "",
|
|
297
|
-
"classification": "clean",
|
|
298
|
-
}
|
|
299
|
-
]
|
|
300
|
-
with patch("subprocess.run") as mock_run:
|
|
301
|
-
mock_run.return_value = _completed(pages_payload)
|
|
302
|
-
reviewer_fetch_core_module.fetch_reviewer_inline_comments(
|
|
303
|
-
_fake_spec(),
|
|
304
|
-
owner="acme",
|
|
305
|
-
repo="widget",
|
|
306
|
-
number=42,
|
|
307
|
-
current_head="abc123",
|
|
308
|
-
all_reviews=matching_review,
|
|
309
|
-
)
|
|
310
|
-
invoked_argv = mock_run.call_args[0][0]
|
|
311
|
-
assert invoked_argv[0] == "gh"
|
|
312
|
-
assert invoked_argv[1] == "api"
|
|
313
|
-
assert "repos/acme/widget/pulls/42/comments?per_page=100" in invoked_argv[2]
|
|
314
|
-
assert "--paginate" in invoked_argv
|
|
315
|
-
assert "--slurp" in invoked_argv
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def test_fetch_reviewer_inline_comments_anchors_to_latest_review_id_for_head() -> None:
|
|
319
|
-
reviews_newest_first = [
|
|
320
|
-
{
|
|
321
|
-
"review_id": 11,
|
|
322
|
-
"commit_id": "same_sha",
|
|
323
|
-
"submitted_at": "2026-01-02T00:00:00Z",
|
|
324
|
-
"state": "APPROVED",
|
|
325
|
-
"body": "lgtm",
|
|
326
|
-
"classification": "clean",
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
"review_id": 10,
|
|
330
|
-
"commit_id": "same_sha",
|
|
331
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
332
|
-
"state": "CHANGES_REQUESTED",
|
|
333
|
-
"body": "fix",
|
|
334
|
-
"classification": "dirty",
|
|
335
|
-
},
|
|
336
|
-
]
|
|
337
|
-
pages_payload = json.dumps(
|
|
338
|
-
[
|
|
339
|
-
[
|
|
340
|
-
{
|
|
341
|
-
"id": 100,
|
|
342
|
-
"user": {"login": "test[bot]"},
|
|
343
|
-
"commit_id": "same_sha",
|
|
344
|
-
"pull_request_review_id": 10,
|
|
345
|
-
"body": "stale",
|
|
346
|
-
"path": "x.py",
|
|
347
|
-
"line": 1,
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
"id": 101,
|
|
351
|
-
"user": {"login": "test[bot]"},
|
|
352
|
-
"commit_id": "same_sha",
|
|
353
|
-
"pull_request_review_id": 11,
|
|
354
|
-
"body": "current",
|
|
355
|
-
"path": "x.py",
|
|
356
|
-
"line": 2,
|
|
357
|
-
},
|
|
358
|
-
]
|
|
359
|
-
]
|
|
360
|
-
)
|
|
361
|
-
with patch("subprocess.run") as mock_run:
|
|
362
|
-
mock_run.return_value = _completed(pages_payload)
|
|
363
|
-
all_inline_comments = reviewer_fetch_core_module.fetch_reviewer_inline_comments(
|
|
364
|
-
_fake_spec(),
|
|
365
|
-
owner="acme",
|
|
366
|
-
repo="widget",
|
|
367
|
-
number=42,
|
|
368
|
-
current_head="same_sha",
|
|
369
|
-
all_reviews=reviews_newest_first,
|
|
370
|
-
)
|
|
371
|
-
assert [each_comment["comment_id"] for each_comment in all_inline_comments] == [101]
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
def test_fetch_reviewer_inline_comments_filters_login_substring() -> None:
|
|
375
|
-
matching_review = [
|
|
376
|
-
{
|
|
377
|
-
"review_id": 9,
|
|
378
|
-
"commit_id": "abc",
|
|
379
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
380
|
-
"state": "APPROVED",
|
|
381
|
-
"body": "",
|
|
382
|
-
"classification": "clean",
|
|
383
|
-
}
|
|
384
|
-
]
|
|
385
|
-
pages_payload = json.dumps(
|
|
386
|
-
[
|
|
387
|
-
[
|
|
388
|
-
{
|
|
389
|
-
"id": 1,
|
|
390
|
-
"user": {"login": "test[bot]"},
|
|
391
|
-
"commit_id": "abc",
|
|
392
|
-
"pull_request_review_id": 9,
|
|
393
|
-
"body": "match",
|
|
394
|
-
"path": "f.py",
|
|
395
|
-
"line": 1,
|
|
396
|
-
},
|
|
397
|
-
{
|
|
398
|
-
"id": 2,
|
|
399
|
-
"user": {"login": "other-reviewer"},
|
|
400
|
-
"commit_id": "abc",
|
|
401
|
-
"pull_request_review_id": 9,
|
|
402
|
-
"body": "no match",
|
|
403
|
-
"path": "f.py",
|
|
404
|
-
"line": 2,
|
|
405
|
-
},
|
|
406
|
-
]
|
|
407
|
-
]
|
|
408
|
-
)
|
|
409
|
-
with patch("subprocess.run") as mock_run:
|
|
410
|
-
mock_run.return_value = _completed(pages_payload)
|
|
411
|
-
all_inline_comments = reviewer_fetch_core_module.fetch_reviewer_inline_comments(
|
|
412
|
-
_fake_spec(login_filter_substring="test"),
|
|
413
|
-
owner="acme",
|
|
414
|
-
repo="widget",
|
|
415
|
-
number=42,
|
|
416
|
-
current_head="abc",
|
|
417
|
-
all_reviews=matching_review,
|
|
418
|
-
)
|
|
419
|
-
assert [each_comment["comment_id"] for each_comment in all_inline_comments] == [1]
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def test_fetch_reviewer_inline_comments_propagates_subprocess_errors() -> None:
|
|
423
|
-
matching_review = [
|
|
424
|
-
{
|
|
425
|
-
"review_id": 1,
|
|
426
|
-
"commit_id": "abc",
|
|
427
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
428
|
-
"state": "APPROVED",
|
|
429
|
-
"body": "",
|
|
430
|
-
"classification": "clean",
|
|
431
|
-
}
|
|
432
|
-
]
|
|
433
|
-
failure = subprocess.CalledProcessError(
|
|
434
|
-
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
435
|
-
)
|
|
436
|
-
with patch("subprocess.run", side_effect=failure):
|
|
437
|
-
try:
|
|
438
|
-
reviewer_fetch_core_module.fetch_reviewer_inline_comments(
|
|
439
|
-
_fake_spec(),
|
|
440
|
-
owner="acme",
|
|
441
|
-
repo="widget",
|
|
442
|
-
number=42,
|
|
443
|
-
current_head="abc",
|
|
444
|
-
all_reviews=matching_review,
|
|
445
|
-
)
|
|
446
|
-
assert False, "expected CalledProcessError"
|
|
447
|
-
except subprocess.CalledProcessError:
|
|
448
|
-
pass
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
"""Tests for reviewer_specs.
|
|
2
|
-
|
|
3
|
-
Covers:
|
|
4
|
-
- each ReviewerSpec instance carries the documented login_filter_substring
|
|
5
|
-
- bugbot_spec.classify_review uses the dirty-body regex
|
|
6
|
-
- copilot_spec.classify_review dispatches off review state plus body
|
|
7
|
-
- claude_spec.classify_review dispatches off review state plus body
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import importlib.util
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from types import ModuleType
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _load_module() -> ModuleType:
|
|
18
|
-
module_path = Path(__file__).parent / "reviewer_specs.py"
|
|
19
|
-
spec = importlib.util.spec_from_file_location("reviewer_specs", module_path)
|
|
20
|
-
assert spec is not None
|
|
21
|
-
assert spec.loader is not None
|
|
22
|
-
module = importlib.util.module_from_spec(spec)
|
|
23
|
-
spec.loader.exec_module(module)
|
|
24
|
-
return module
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
reviewer_specs_module = _load_module()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_bugbot_spec_uses_cursor_login_filter_substring() -> None:
|
|
31
|
-
assert reviewer_specs_module.bugbot_spec.login_filter_substring == "cursor"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_copilot_spec_uses_copilot_login_filter_substring() -> None:
|
|
35
|
-
assert reviewer_specs_module.copilot_spec.login_filter_substring == "copilot"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_claude_spec_uses_claude_login_filter_substring() -> None:
|
|
39
|
-
assert reviewer_specs_module.claude_spec.login_filter_substring == "claude"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_bugbot_classify_returns_dirty_when_body_matches_findings_pattern() -> None:
|
|
43
|
-
review_payload = {
|
|
44
|
-
"body": "Cursor Bugbot has reviewed your changes and found 2 potential issues.",
|
|
45
|
-
}
|
|
46
|
-
assert reviewer_specs_module.bugbot_spec.classify_review(review_payload) == "dirty"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_bugbot_classify_returns_clean_when_body_lacks_findings_pattern() -> None:
|
|
50
|
-
review_payload = {
|
|
51
|
-
"body": "Bugbot reviewed your changes and found no new issues!",
|
|
52
|
-
}
|
|
53
|
-
assert reviewer_specs_module.bugbot_spec.classify_review(review_payload) == "clean"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_copilot_classify_returns_clean_when_state_is_approved() -> None:
|
|
57
|
-
review_payload = {"state": "APPROVED", "body": "lgtm"}
|
|
58
|
-
assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "clean"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_copilot_classify_returns_dirty_when_state_is_changes_requested() -> None:
|
|
62
|
-
review_payload = {"state": "CHANGES_REQUESTED", "body": "fix this"}
|
|
63
|
-
assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "dirty"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_copilot_classify_returns_dirty_when_state_is_commented_with_body() -> None:
|
|
67
|
-
review_payload = {"state": "COMMENTED", "body": "minor nit"}
|
|
68
|
-
assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "dirty"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_copilot_classify_returns_clean_when_state_is_commented_with_empty_body() -> (
|
|
72
|
-
None
|
|
73
|
-
):
|
|
74
|
-
review_payload = {"state": "COMMENTED", "body": ""}
|
|
75
|
-
assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "clean"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_copilot_classify_returns_clean_when_state_is_unknown() -> None:
|
|
79
|
-
review_payload = {"state": "DISMISSED", "body": "ignored"}
|
|
80
|
-
assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "clean"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_claude_classify_returns_clean_when_state_is_approved() -> None:
|
|
84
|
-
review_payload = {"state": "APPROVED", "body": "lgtm"}
|
|
85
|
-
assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "clean"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_claude_classify_returns_dirty_when_state_is_changes_requested() -> None:
|
|
89
|
-
review_payload = {"state": "CHANGES_REQUESTED", "body": "fix this"}
|
|
90
|
-
assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "dirty"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_claude_classify_returns_dirty_when_state_is_commented_with_body() -> None:
|
|
94
|
-
review_payload = {"state": "COMMENTED", "body": "minor nit"}
|
|
95
|
-
assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "dirty"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def test_claude_classify_returns_clean_when_state_is_commented_with_empty_body() -> (
|
|
99
|
-
None
|
|
100
|
-
):
|
|
101
|
-
review_payload = {"state": "COMMENTED", "body": ""}
|
|
102
|
-
assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "clean"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def test_claude_classify_returns_clean_when_state_is_unknown() -> None:
|
|
106
|
-
review_payload = {"state": "DISMISSED", "body": "ignored"}
|
|
107
|
-
assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "clean"
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
"""Tests for trigger_bugbot.
|
|
2
|
-
|
|
3
|
-
Covers:
|
|
4
|
-
- gh pr comment is invoked with --body-file (per gh-body-file rule)
|
|
5
|
-
- the body file written contains the literal phrase "bugbot run\\n"
|
|
6
|
-
- the comment URL emitted by gh is returned
|
|
7
|
-
- the temp body file is cleaned up
|
|
8
|
-
- subprocess errors propagate
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import importlib.util
|
|
14
|
-
import subprocess
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from types import ModuleType
|
|
17
|
-
from unittest.mock import MagicMock, patch
|
|
18
|
-
|
|
19
|
-
import pytest
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _load_module() -> ModuleType:
|
|
23
|
-
module_path = Path(__file__).parent / "trigger_bugbot.py"
|
|
24
|
-
spec = importlib.util.spec_from_file_location("trigger_bugbot", module_path)
|
|
25
|
-
assert spec is not None
|
|
26
|
-
assert spec.loader is not None
|
|
27
|
-
module = importlib.util.module_from_spec(spec)
|
|
28
|
-
spec.loader.exec_module(module)
|
|
29
|
-
return module
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
trigger_bugbot_module = _load_module()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _completed(stdout: str) -> subprocess.CompletedProcess:
|
|
36
|
-
process = MagicMock(spec=subprocess.CompletedProcess)
|
|
37
|
-
process.stdout = stdout
|
|
38
|
-
process.returncode = 0
|
|
39
|
-
return process
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_should_invoke_gh_pr_comment_with_body_file_flag() -> None:
|
|
43
|
-
captured_body_paths: list[str] = []
|
|
44
|
-
|
|
45
|
-
def capture_body_file_contents(*subprocess_args, **_subprocess_kwargs):
|
|
46
|
-
invoked_argv = subprocess_args[0]
|
|
47
|
-
assert "--body-file" in invoked_argv
|
|
48
|
-
body_file_path = invoked_argv[invoked_argv.index("--body-file") + 1]
|
|
49
|
-
captured_body_paths.append(body_file_path)
|
|
50
|
-
return _completed("https://github.com/acme/widget/issues/42#issuecomment-99\n")
|
|
51
|
-
|
|
52
|
-
with patch("subprocess.run", side_effect=capture_body_file_contents) as mock_run:
|
|
53
|
-
trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
|
|
54
|
-
invoked_argv = mock_run.call_args[0][0]
|
|
55
|
-
assert invoked_argv[0:3] == ["gh", "pr", "comment"]
|
|
56
|
-
assert "42" in invoked_argv
|
|
57
|
-
assert "--repo" in invoked_argv
|
|
58
|
-
assert "acme/widget" in invoked_argv
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_should_write_literal_bugbot_run_phrase_into_body_file() -> None:
|
|
62
|
-
captured_body_contents: list[str] = []
|
|
63
|
-
|
|
64
|
-
def capture_body_file_contents(*subprocess_args, **_subprocess_kwargs):
|
|
65
|
-
invoked_argv = subprocess_args[0]
|
|
66
|
-
body_file_path = Path(invoked_argv[invoked_argv.index("--body-file") + 1])
|
|
67
|
-
captured_body_contents.append(body_file_path.read_text(encoding="utf-8"))
|
|
68
|
-
return _completed("https://example.com\n")
|
|
69
|
-
|
|
70
|
-
with patch("subprocess.run", side_effect=capture_body_file_contents):
|
|
71
|
-
trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
|
|
72
|
-
assert len(captured_body_contents) == 1
|
|
73
|
-
assert captured_body_contents[0] == "bugbot run\n"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def test_should_return_comment_url_from_gh_stdout() -> None:
|
|
77
|
-
expected_url = "https://github.com/acme/widget/pull/42#issuecomment-12345"
|
|
78
|
-
with patch("subprocess.run") as mock_run:
|
|
79
|
-
mock_run.return_value = _completed(f"{expected_url}\n")
|
|
80
|
-
comment_url = trigger_bugbot_module.trigger_bugbot(
|
|
81
|
-
owner="acme", repo="widget", number=42
|
|
82
|
-
)
|
|
83
|
-
assert comment_url == expected_url
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def test_should_remove_temp_body_file_after_invocation() -> None:
|
|
87
|
-
captured_body_paths: list[Path] = []
|
|
88
|
-
|
|
89
|
-
def capture_body_file_contents(*subprocess_args, **_subprocess_kwargs):
|
|
90
|
-
invoked_argv = subprocess_args[0]
|
|
91
|
-
captured_body_paths.append(
|
|
92
|
-
Path(invoked_argv[invoked_argv.index("--body-file") + 1])
|
|
93
|
-
)
|
|
94
|
-
return _completed("https://example.com\n")
|
|
95
|
-
|
|
96
|
-
with patch("subprocess.run", side_effect=capture_body_file_contents):
|
|
97
|
-
trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
|
|
98
|
-
assert len(captured_body_paths) == 1
|
|
99
|
-
assert not captured_body_paths[0].exists()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def test_should_raise_when_gh_subprocess_fails() -> None:
|
|
103
|
-
failure = subprocess.CalledProcessError(
|
|
104
|
-
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
105
|
-
)
|
|
106
|
-
with patch("subprocess.run", side_effect=failure):
|
|
107
|
-
with pytest.raises(subprocess.CalledProcessError):
|
|
108
|
-
trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def test_should_write_imported_constant_directly_without_local_alias() -> None:
|
|
112
|
-
captured_body_contents: list[str] = []
|
|
113
|
-
|
|
114
|
-
def capture_body_file_contents(*subprocess_args, **_subprocess_kwargs):
|
|
115
|
-
invoked_argv = subprocess_args[0]
|
|
116
|
-
body_file_path = Path(invoked_argv[invoked_argv.index("--body-file") + 1])
|
|
117
|
-
captured_body_contents.append(body_file_path.read_text(encoding="utf-8"))
|
|
118
|
-
return _completed("https://example.com\n")
|
|
119
|
-
|
|
120
|
-
with patch("subprocess.run", side_effect=capture_body_file_contents):
|
|
121
|
-
trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=99)
|
|
122
|
-
assert len(captured_body_contents) == 1
|
|
123
|
-
assert (
|
|
124
|
-
captured_body_contents[0]
|
|
125
|
-
== trigger_bugbot_module.BUGBOT_RUN_TRIGGER_PHRASE
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def test_should_render_repo_arg_via_named_template_constant() -> None:
|
|
130
|
-
with patch("subprocess.run") as mock_run:
|
|
131
|
-
mock_run.return_value = _completed("https://example.com\n")
|
|
132
|
-
trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
|
|
133
|
-
invoked_argv = mock_run.call_args[0][0]
|
|
134
|
-
expected_repo_arg = trigger_bugbot_module.GH_REPO_ARG_TEMPLATE.format(
|
|
135
|
-
owner="acme", repo="widget"
|
|
136
|
-
)
|
|
137
|
-
assert expected_repo_arg == "acme/widget"
|
|
138
|
-
repo_flag_index = invoked_argv.index("--repo")
|
|
139
|
-
assert invoked_argv[repo_flag_index + 1] == expected_repo_arg
|