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.
Files changed (95) hide show
  1. package/CLAUDE.md +3 -0
  2. package/_shared/pr-loop/audit-contract.md +4 -3
  3. package/_shared/pr-loop/fix-protocol.md +2 -0
  4. package/_shared/pr-loop/gh-payloads.md +38 -37
  5. package/_shared/pr-loop/scripts/README.md +0 -1
  6. package/_shared/pr-loop/scripts/preflight.py +2 -1
  7. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +2 -2
  8. package/_shared/pr-loop/scripts/tests/test_preflight.py +22 -0
  9. package/_shared/pr-loop/state-schema.md +10 -10
  10. package/agents/clean-coder.md +4 -0
  11. package/agents/code-quality-agent.md +23 -85
  12. package/agents/groq-coder.md +8 -6
  13. package/hooks/blocking/__init__.py +0 -0
  14. package/hooks/blocking/hedging_language_blocker.py +2 -2
  15. package/hooks/blocking/state_description_blocker.py +243 -0
  16. package/hooks/blocking/tdd_enforcer.py +94 -0
  17. package/hooks/blocking/test_hedging_language_blocker.py +1 -1
  18. package/hooks/blocking/test_state_description_blocker.py +618 -0
  19. package/hooks/blocking/test_tdd_enforcer.py +152 -0
  20. package/hooks/config/state_description_blocker_constants.py +130 -0
  21. package/hooks/hooks.json +10 -0
  22. package/package.json +1 -1
  23. package/rules/gh-paginate.md +4 -50
  24. package/rules/no-historical-clutter.md +57 -0
  25. package/scripts/config/groq_bugteam_config.py +13 -5
  26. package/skills/bugteam/CONSTRAINTS.md +20 -27
  27. package/skills/bugteam/EXAMPLES.md +1 -1
  28. package/skills/bugteam/PROMPTS.md +78 -42
  29. package/skills/bugteam/SKILL.md +76 -63
  30. package/skills/bugteam/SKILL_EVALS.md +12 -12
  31. package/skills/bugteam/reference/audit-and-teammates.md +21 -48
  32. package/skills/bugteam/reference/audit-contract.md +7 -7
  33. package/skills/bugteam/reference/github-pr-reviews.md +31 -31
  34. package/skills/bugteam/reference/team-setup.md +1 -1
  35. package/skills/bugteam/reference/teardown-publish-permissions.md +4 -4
  36. package/skills/copilot-review/SKILL.md +7 -14
  37. package/skills/findbugs/SKILL.md +2 -2
  38. package/skills/fixbugs/SKILL.md +1 -1
  39. package/skills/monitor-open-prs/SKILL.md +6 -6
  40. package/skills/pr-converge/SKILL.md +7 -6
  41. package/skills/pr-converge/reference/convergence-gates.md +46 -44
  42. package/skills/pr-converge/reference/examples.md +4 -4
  43. package/skills/pr-converge/reference/fix-protocol.md +8 -8
  44. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -10
  45. package/skills/pr-converge/reference/per-tick.md +24 -36
  46. package/skills/pr-converge/reference/stop-conditions.md +7 -7
  47. package/skills/pr-converge/scripts/README.md +65 -117
  48. package/skills/pr-review-responder/EXAMPLES.md +2 -2
  49. package/skills/pr-review-responder/PRINCIPLES.md +2 -8
  50. package/skills/pr-review-responder/README.md +7 -48
  51. package/skills/pr-review-responder/SKILL.md +2 -3
  52. package/skills/pr-review-responder/TESTING.md +8 -65
  53. package/skills/qbug/SKILL.md +10 -16
  54. package/_shared/pr-loop/scripts/config/gh_util_constants.py +0 -31
  55. package/_shared/pr-loop/scripts/gh_util.py +0 -193
  56. package/_shared/pr-loop/scripts/tests/test_gh_util.py +0 -257
  57. package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +0 -61
  58. package/skills/pr-converge/scripts/check_pr_mergeability.py +0 -78
  59. package/skills/pr-converge/scripts/config/pr_converge_constants.py +0 -118
  60. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -152
  61. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +0 -70
  62. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +0 -57
  63. package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +0 -70
  64. package/skills/pr-converge/scripts/fetch_claude_reviews.py +0 -61
  65. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +0 -70
  66. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +0 -61
  67. package/skills/pr-converge/scripts/mark_pr_ready.py +0 -54
  68. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +0 -49
  69. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +0 -33
  70. package/skills/pr-converge/scripts/reply_to_inline_comment.py +0 -84
  71. package/skills/pr-converge/scripts/request_copilot_review.py +0 -71
  72. package/skills/pr-converge/scripts/resolve_pr_head.py +0 -58
  73. package/skills/pr-converge/scripts/review_field_helpers.py +0 -43
  74. package/skills/pr-converge/scripts/reviewer_fetch_core.py +0 -153
  75. package/skills/pr-converge/scripts/reviewer_specs.py +0 -98
  76. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +0 -126
  77. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +0 -443
  78. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +0 -299
  79. package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +0 -485
  80. package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +0 -368
  81. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +0 -440
  82. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +0 -366
  83. package/skills/pr-converge/scripts/test_mark_pr_ready.py +0 -69
  84. package/skills/pr-converge/scripts/test_post_bugbot_run.py +0 -195
  85. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +0 -159
  86. package/skills/pr-converge/scripts/test_request_copilot_review.py +0 -101
  87. package/skills/pr-converge/scripts/test_resolve_pr_head.py +0 -79
  88. package/skills/pr-converge/scripts/test_review_field_helpers.py +0 -80
  89. package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +0 -448
  90. package/skills/pr-converge/scripts/test_reviewer_specs.py +0 -107
  91. package/skills/pr-converge/scripts/test_trigger_bugbot.py +0 -139
  92. package/skills/pr-converge/scripts/test_view_pr_context.py +0 -111
  93. package/skills/pr-converge/scripts/trigger_bugbot.py +0 -77
  94. package/skills/pr-converge/scripts/view_pr_context.py +0 -47
  95. 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