claude-dev-env 1.36.2 → 1.37.1
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/_shared/pr-loop/scripts/config/preflight_constants.py +29 -8
- package/_shared/pr-loop/scripts/preflight.py +242 -20
- package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
- package/hooks/blocking/code_rules_enforcer.py +269 -23
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
- package/hooks/config/test_unused_module_import_constants.py +48 -0
- package/hooks/config/unused_module_import_constants.py +41 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +4 -50
- package/rules/no-historical-clutter.md +36 -0
- package/skills/bg-agent/SKILL.md +69 -0
- package/skills/bugteam/CONSTRAINTS.md +10 -19
- package/skills/bugteam/PROMPTS.md +21 -14
- package/skills/bugteam/SKILL.md +122 -208
- package/skills/bugteam/SKILL_EVALS.md +75 -114
- package/skills/bugteam/reference/README.md +2 -4
- package/skills/bugteam/reference/audit-and-teammates.md +21 -48
- package/skills/bugteam/reference/audit-contract.md +7 -7
- package/skills/bugteam/reference/design-rationale.md +3 -8
- package/skills/bugteam/reference/team-setup.md +11 -19
- package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
- package/skills/bugteam/scripts/config/__init__.py +0 -0
- package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
- package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
- package/skills/bugteam/sources.md +1 -25
- package/skills/bugteam/test_skill_additions.py +4 -13
- package/skills/fresh-branch/SKILL.md +71 -0
- package/skills/gotcha/SKILL.md +73 -0
- package/skills/monitor-open-prs/SKILL.md +4 -37
- package/skills/monitor-open-prs/test_skill_contract.py +0 -5
- package/skills/pr-converge/SKILL.md +60 -1298
- package/skills/pr-converge/reference/convergence-gates.md +122 -0
- package/skills/pr-converge/reference/examples.md +76 -0
- package/skills/pr-converge/reference/fix-protocol.md +56 -0
- package/skills/pr-converge/reference/ground-rules.md +13 -0
- package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
- package/skills/pr-converge/reference/per-tick.md +204 -0
- package/skills/pr-converge/reference/state-schema.md +19 -0
- package/skills/pr-converge/reference/stop-conditions.md +26 -0
- package/skills/pr-converge/scripts/README.md +36 -9
- package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +74 -5
- package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
- package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
- package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
- package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
- package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
- package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
- package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
- package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
- package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
- package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
- package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +44 -0
- package/skills/pr-converge/scripts/view_pr_context.py +35 -4
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
- package/skills/bugteam/test_team_lifecycle.py +0 -103
- package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
- package/skills/pr-converge/test_team_lifecycle.py +0 -56
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +0 -108
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Tests for fetch_claude_reviews.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- gh command uses --paginate --slurp (per gh-paginate rule)
|
|
5
|
+
- per-page filter happens in Python after fetching all pages
|
|
6
|
+
- only claude reviewer-bot reviews are returned (case-insensitive substring match)
|
|
7
|
+
- reviews are sorted newest-first by submitted_at
|
|
8
|
+
- reviews with state APPROVED are classified "clean"
|
|
9
|
+
- reviews with state CHANGES_REQUESTED or COMMENTED are classified "dirty"
|
|
10
|
+
- subprocess errors propagate with stderr context
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib.util
|
|
16
|
+
import json
|
|
17
|
+
import subprocess
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from types import ModuleType
|
|
20
|
+
from unittest.mock import MagicMock, patch
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_module() -> ModuleType:
|
|
26
|
+
module_path = Path(__file__).parent / "fetch_claude_reviews.py"
|
|
27
|
+
spec = importlib.util.spec_from_file_location("fetch_claude_reviews", 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
|
+
fetch_claude_reviews_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 test_should_invoke_gh_with_paginate_slurp_against_reviews_endpoint() -> None:
|
|
46
|
+
pages_payload = json.dumps([[]])
|
|
47
|
+
with patch("subprocess.run") as mock_run:
|
|
48
|
+
mock_run.return_value = _completed(pages_payload)
|
|
49
|
+
fetch_claude_reviews_module.fetch_claude_reviews(
|
|
50
|
+
owner="acme", repo="widget", number=42
|
|
51
|
+
)
|
|
52
|
+
invoked_argv = mock_run.call_args[0][0]
|
|
53
|
+
assert invoked_argv[0] == "gh"
|
|
54
|
+
assert invoked_argv[1] == "api"
|
|
55
|
+
assert "repos/acme/widget/pulls/42/reviews?per_page=100" in invoked_argv[2]
|
|
56
|
+
assert "--paginate" in invoked_argv
|
|
57
|
+
assert "--slurp" in invoked_argv
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_should_filter_to_claude_reviewer_only() -> None:
|
|
61
|
+
pages_payload = json.dumps(
|
|
62
|
+
[
|
|
63
|
+
[
|
|
64
|
+
{
|
|
65
|
+
"id": 1,
|
|
66
|
+
"user": {"login": "cursor[bot]"},
|
|
67
|
+
"state": "COMMENTED",
|
|
68
|
+
"commit_id": "abc",
|
|
69
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
70
|
+
"body": "bugbot stuff",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": 2,
|
|
74
|
+
"user": {"login": "claude[bot]"},
|
|
75
|
+
"state": "CHANGES_REQUESTED",
|
|
76
|
+
"commit_id": "abc",
|
|
77
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
78
|
+
"body": "claude finding",
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
with patch("subprocess.run") as mock_run:
|
|
84
|
+
mock_run.return_value = _completed(pages_payload)
|
|
85
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
86
|
+
owner="acme", repo="widget", number=42
|
|
87
|
+
)
|
|
88
|
+
assert len(all_reviews) == 1
|
|
89
|
+
assert all_reviews[0]["review_id"] == 2
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_should_return_reviews_newest_first_across_pages() -> None:
|
|
93
|
+
pages_payload = json.dumps(
|
|
94
|
+
[
|
|
95
|
+
[
|
|
96
|
+
{
|
|
97
|
+
"id": 10,
|
|
98
|
+
"user": {"login": "claude[bot]"},
|
|
99
|
+
"state": "APPROVED",
|
|
100
|
+
"commit_id": "old",
|
|
101
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
102
|
+
"body": "lgtm",
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
{
|
|
107
|
+
"id": 11,
|
|
108
|
+
"user": {"login": "claude[bot]"},
|
|
109
|
+
"state": "CHANGES_REQUESTED",
|
|
110
|
+
"commit_id": "new",
|
|
111
|
+
"submitted_at": "2026-01-03T00:00:00Z",
|
|
112
|
+
"body": "issues found",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": 12,
|
|
116
|
+
"user": {"login": "claude[bot]"},
|
|
117
|
+
"state": "APPROVED",
|
|
118
|
+
"commit_id": "mid",
|
|
119
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
120
|
+
"body": "lgtm",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
with patch("subprocess.run") as mock_run:
|
|
126
|
+
mock_run.return_value = _completed(pages_payload)
|
|
127
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
128
|
+
owner="acme", repo="widget", number=42
|
|
129
|
+
)
|
|
130
|
+
submitted_at_sequence = [each_review["submitted_at"] for each_review in all_reviews]
|
|
131
|
+
assert submitted_at_sequence == [
|
|
132
|
+
"2026-01-03T00:00:00Z",
|
|
133
|
+
"2026-01-02T00:00:00Z",
|
|
134
|
+
"2026-01-01T00:00:00Z",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_should_classify_dirty_review_when_state_is_changes_requested() -> None:
|
|
139
|
+
pages_payload = json.dumps(
|
|
140
|
+
[
|
|
141
|
+
[
|
|
142
|
+
{
|
|
143
|
+
"id": 1,
|
|
144
|
+
"user": {"login": "claude[bot]"},
|
|
145
|
+
"state": "CHANGES_REQUESTED",
|
|
146
|
+
"commit_id": "abc",
|
|
147
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
148
|
+
"body": "Issues need addressing.",
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
with patch("subprocess.run") as mock_run:
|
|
154
|
+
mock_run.return_value = _completed(pages_payload)
|
|
155
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
156
|
+
owner="acme", repo="widget", number=42
|
|
157
|
+
)
|
|
158
|
+
assert all_reviews[0]["classification"] == "dirty"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_should_classify_dirty_review_when_state_is_commented_with_body() -> None:
|
|
162
|
+
pages_payload = json.dumps(
|
|
163
|
+
[
|
|
164
|
+
[
|
|
165
|
+
{
|
|
166
|
+
"id": 1,
|
|
167
|
+
"user": {"login": "claude[bot]"},
|
|
168
|
+
"state": "COMMENTED",
|
|
169
|
+
"commit_id": "abc",
|
|
170
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
171
|
+
"body": "Found a couple of nits inline.",
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
with patch("subprocess.run") as mock_run:
|
|
177
|
+
mock_run.return_value = _completed(pages_payload)
|
|
178
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
179
|
+
owner="acme", repo="widget", number=42
|
|
180
|
+
)
|
|
181
|
+
assert all_reviews[0]["classification"] == "dirty"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_should_classify_clean_review_when_state_is_commented_with_empty_body() -> None:
|
|
185
|
+
pages_payload = json.dumps(
|
|
186
|
+
[
|
|
187
|
+
[
|
|
188
|
+
{
|
|
189
|
+
"id": 1,
|
|
190
|
+
"user": {"login": "claude[bot]"},
|
|
191
|
+
"state": "COMMENTED",
|
|
192
|
+
"commit_id": "abc",
|
|
193
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
194
|
+
"body": "",
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
]
|
|
198
|
+
)
|
|
199
|
+
with patch("subprocess.run") as mock_run:
|
|
200
|
+
mock_run.return_value = _completed(pages_payload)
|
|
201
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
202
|
+
owner="acme", repo="widget", number=42
|
|
203
|
+
)
|
|
204
|
+
assert all_reviews[0]["classification"] == "clean"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_should_dispatch_dirty_classification_off_claude_dirty_review_states_tuple() -> (
|
|
208
|
+
None
|
|
209
|
+
):
|
|
210
|
+
source_text = (
|
|
211
|
+
Path(__file__).resolve().parent / "reviewer_specs.py"
|
|
212
|
+
).read_text(encoding="utf-8")
|
|
213
|
+
assert "ALL_CLAUDE_DIRTY_REVIEW_STATES" in source_text
|
|
214
|
+
assert "all_dirty_states=ALL_CLAUDE_DIRTY_REVIEW_STATES" in source_text
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_should_classify_clean_review_when_state_is_approved() -> None:
|
|
218
|
+
pages_payload = json.dumps(
|
|
219
|
+
[
|
|
220
|
+
[
|
|
221
|
+
{
|
|
222
|
+
"id": 1,
|
|
223
|
+
"user": {"login": "claude[bot]"},
|
|
224
|
+
"state": "APPROVED",
|
|
225
|
+
"commit_id": "abc",
|
|
226
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
227
|
+
"body": "looks good",
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
]
|
|
231
|
+
)
|
|
232
|
+
with patch("subprocess.run") as mock_run:
|
|
233
|
+
mock_run.return_value = _completed(pages_payload)
|
|
234
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
235
|
+
owner="acme", repo="widget", number=42
|
|
236
|
+
)
|
|
237
|
+
assert all_reviews[0]["classification"] == "clean"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_should_match_login_case_insensitively() -> None:
|
|
241
|
+
pages_payload = json.dumps(
|
|
242
|
+
[
|
|
243
|
+
[
|
|
244
|
+
{
|
|
245
|
+
"id": 1,
|
|
246
|
+
"user": {"login": "Claude"},
|
|
247
|
+
"state": "CHANGES_REQUESTED",
|
|
248
|
+
"commit_id": "abc",
|
|
249
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
250
|
+
"body": "uppercase login",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": 2,
|
|
254
|
+
"user": {"login": "CLAUDE-CODE[bot]"},
|
|
255
|
+
"state": "APPROVED",
|
|
256
|
+
"commit_id": "abc",
|
|
257
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
258
|
+
"body": "screaming login",
|
|
259
|
+
},
|
|
260
|
+
]
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
with patch("subprocess.run") as mock_run:
|
|
264
|
+
mock_run.return_value = _completed(pages_payload)
|
|
265
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
266
|
+
owner="acme", repo="widget", number=42
|
|
267
|
+
)
|
|
268
|
+
assert len(all_reviews) == 2
|
|
269
|
+
assert {each_review["review_id"] for each_review in all_reviews} == {1, 2}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_should_match_login_containing_claude_substring() -> None:
|
|
273
|
+
pages_payload = json.dumps(
|
|
274
|
+
[
|
|
275
|
+
[
|
|
276
|
+
{
|
|
277
|
+
"id": 1,
|
|
278
|
+
"user": {"login": "claude[bot]"},
|
|
279
|
+
"state": "CHANGES_REQUESTED",
|
|
280
|
+
"commit_id": "abc",
|
|
281
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
282
|
+
"body": "canonical bot login",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"id": 2,
|
|
286
|
+
"user": {"login": "anthropic-claude[bot]"},
|
|
287
|
+
"state": "CHANGES_REQUESTED",
|
|
288
|
+
"commit_id": "abc",
|
|
289
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
290
|
+
"body": "non-canonical login still matches",
|
|
291
|
+
},
|
|
292
|
+
]
|
|
293
|
+
]
|
|
294
|
+
)
|
|
295
|
+
with patch("subprocess.run") as mock_run:
|
|
296
|
+
mock_run.return_value = _completed(pages_payload)
|
|
297
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
298
|
+
owner="acme", repo="widget", number=42
|
|
299
|
+
)
|
|
300
|
+
assert {each_review["review_id"] for each_review in all_reviews} == {1, 2}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_should_exclude_login_without_claude_substring() -> None:
|
|
304
|
+
pages_payload = json.dumps(
|
|
305
|
+
[
|
|
306
|
+
[
|
|
307
|
+
{
|
|
308
|
+
"id": 1,
|
|
309
|
+
"user": {"login": "copilot-pull-request-reviewer[bot]"},
|
|
310
|
+
"state": "CHANGES_REQUESTED",
|
|
311
|
+
"commit_id": "abc",
|
|
312
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
313
|
+
"body": "wrong bot",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"id": 2,
|
|
317
|
+
"user": {"login": "dependabot[bot]"},
|
|
318
|
+
"state": "CHANGES_REQUESTED",
|
|
319
|
+
"commit_id": "abc",
|
|
320
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
321
|
+
"body": "also wrong",
|
|
322
|
+
},
|
|
323
|
+
]
|
|
324
|
+
]
|
|
325
|
+
)
|
|
326
|
+
with patch("subprocess.run") as mock_run:
|
|
327
|
+
mock_run.return_value = _completed(pages_payload)
|
|
328
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
329
|
+
owner="acme", repo="widget", number=42
|
|
330
|
+
)
|
|
331
|
+
assert all_reviews == []
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_should_raise_when_gh_subprocess_fails() -> None:
|
|
335
|
+
failure = subprocess.CalledProcessError(
|
|
336
|
+
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
337
|
+
)
|
|
338
|
+
with patch("subprocess.run", side_effect=failure):
|
|
339
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
340
|
+
fetch_claude_reviews_module.fetch_claude_reviews(
|
|
341
|
+
owner="acme", repo="widget", number=42
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_should_return_entries_whose_keys_are_strings() -> None:
|
|
346
|
+
pages_payload = json.dumps(
|
|
347
|
+
[
|
|
348
|
+
[
|
|
349
|
+
{
|
|
350
|
+
"id": 1,
|
|
351
|
+
"user": {"login": "claude[bot]"},
|
|
352
|
+
"state": "APPROVED",
|
|
353
|
+
"commit_id": "abc",
|
|
354
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
355
|
+
"body": "looks good",
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
]
|
|
359
|
+
)
|
|
360
|
+
with patch("subprocess.run") as mock_run:
|
|
361
|
+
mock_run.return_value = _completed(pages_payload)
|
|
362
|
+
all_reviews = fetch_claude_reviews_module.fetch_claude_reviews(
|
|
363
|
+
owner="acme", repo="widget", number=42
|
|
364
|
+
)
|
|
365
|
+
assert len(all_reviews) == 1
|
|
366
|
+
first_review_entry = all_reviews[0]
|
|
367
|
+
assert isinstance(first_review_entry, dict)
|
|
368
|
+
assert all(isinstance(each_key, str) for each_key in first_review_entry.keys())
|
|
@@ -310,12 +310,80 @@ def test_should_flatten_across_pages() -> None:
|
|
|
310
310
|
]
|
|
311
311
|
|
|
312
312
|
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
313
|
+
def test_should_match_login_case_insensitively() -> None:
|
|
314
|
+
pages_payload = json.dumps(
|
|
315
|
+
[
|
|
316
|
+
[
|
|
317
|
+
{
|
|
318
|
+
"id": 300,
|
|
319
|
+
"user": {"login": "Copilot"},
|
|
320
|
+
"commit_id": "abc123",
|
|
321
|
+
"pull_request_review_id": 1,
|
|
322
|
+
"body": "uppercase login",
|
|
323
|
+
"path": "x.py",
|
|
324
|
+
"line": 5,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"id": 301,
|
|
328
|
+
"user": {"login": "GITHUB-COPILOT[bot]"},
|
|
329
|
+
"commit_id": "abc123",
|
|
330
|
+
"pull_request_review_id": 1,
|
|
331
|
+
"body": "screaming login",
|
|
332
|
+
"path": "x.py",
|
|
333
|
+
"line": 6,
|
|
334
|
+
},
|
|
335
|
+
]
|
|
336
|
+
]
|
|
337
|
+
)
|
|
338
|
+
with (
|
|
339
|
+
patch.object(
|
|
340
|
+
fetch_copilot_inline_comments_module,
|
|
341
|
+
"fetch_copilot_reviews",
|
|
342
|
+
return_value=_default_review_for_head(commit="abc123", review_id=1),
|
|
343
|
+
),
|
|
344
|
+
patch("subprocess.run") as mock_run,
|
|
345
|
+
):
|
|
346
|
+
mock_run.return_value = _completed(pages_payload)
|
|
347
|
+
all_inline_comments = (
|
|
348
|
+
fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
|
|
349
|
+
owner="acme", repo="widget", number=42, current_head="abc123"
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
assert {each_comment["comment_id"] for each_comment in all_inline_comments} == {300, 301}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_should_match_login_containing_copilot_substring() -> None:
|
|
356
|
+
pages_payload = json.dumps(
|
|
357
|
+
[
|
|
358
|
+
[
|
|
359
|
+
{
|
|
360
|
+
"id": 400,
|
|
361
|
+
"user": {"login": "internal-copilot-fork[bot]"},
|
|
362
|
+
"commit_id": "abc123",
|
|
363
|
+
"pull_request_review_id": 1,
|
|
364
|
+
"body": "non-canonical login still matches",
|
|
365
|
+
"path": "x.py",
|
|
366
|
+
"line": 5,
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
]
|
|
370
|
+
)
|
|
371
|
+
with (
|
|
372
|
+
patch.object(
|
|
373
|
+
fetch_copilot_inline_comments_module,
|
|
374
|
+
"fetch_copilot_reviews",
|
|
375
|
+
return_value=_default_review_for_head(commit="abc123", review_id=1),
|
|
376
|
+
),
|
|
377
|
+
patch("subprocess.run") as mock_run,
|
|
378
|
+
):
|
|
379
|
+
mock_run.return_value = _completed(pages_payload)
|
|
380
|
+
all_inline_comments = (
|
|
381
|
+
fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
|
|
382
|
+
owner="acme", repo="widget", number=42, current_head="abc123"
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
assert len(all_inline_comments) == 1
|
|
386
|
+
assert all_inline_comments[0]["comment_id"] == 400
|
|
319
387
|
|
|
320
388
|
|
|
321
389
|
def test_should_raise_when_gh_subprocess_fails() -> None:
|
|
@@ -206,10 +206,10 @@ def test_should_classify_clean_review_when_state_is_commented_with_empty_body()
|
|
|
206
206
|
|
|
207
207
|
def test_should_dispatch_dirty_classification_off_copilot_dirty_review_states_tuple() -> None:
|
|
208
208
|
source_text = (
|
|
209
|
-
Path(__file__).resolve().parent / "
|
|
209
|
+
Path(__file__).resolve().parent / "reviewer_specs.py"
|
|
210
210
|
).read_text(encoding="utf-8")
|
|
211
211
|
assert "ALL_COPILOT_DIRTY_REVIEW_STATES" in source_text
|
|
212
|
-
assert "
|
|
212
|
+
assert "all_dirty_states=ALL_COPILOT_DIRTY_REVIEW_STATES" in source_text
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
def test_should_classify_clean_review_when_state_is_approved() -> None:
|
|
@@ -235,12 +235,98 @@ def test_should_classify_clean_review_when_state_is_approved() -> None:
|
|
|
235
235
|
assert all_reviews[0]["classification"] == "clean"
|
|
236
236
|
|
|
237
237
|
|
|
238
|
-
def
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
def test_should_match_login_case_insensitively() -> None:
|
|
239
|
+
pages_payload = json.dumps(
|
|
240
|
+
[
|
|
241
|
+
[
|
|
242
|
+
{
|
|
243
|
+
"id": 1,
|
|
244
|
+
"user": {"login": "Copilot"},
|
|
245
|
+
"state": "CHANGES_REQUESTED",
|
|
246
|
+
"commit_id": "abc",
|
|
247
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
248
|
+
"body": "uppercase login",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"id": 2,
|
|
252
|
+
"user": {"login": "GITHUB-COPILOT[bot]"},
|
|
253
|
+
"state": "APPROVED",
|
|
254
|
+
"commit_id": "abc",
|
|
255
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
256
|
+
"body": "screaming login",
|
|
257
|
+
},
|
|
258
|
+
]
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
with patch("subprocess.run") as mock_run:
|
|
262
|
+
mock_run.return_value = _completed(pages_payload)
|
|
263
|
+
all_reviews = fetch_copilot_reviews_module.fetch_copilot_reviews(
|
|
264
|
+
owner="acme", repo="widget", number=42
|
|
265
|
+
)
|
|
266
|
+
assert len(all_reviews) == 2
|
|
267
|
+
assert {each_review["review_id"] for each_review in all_reviews} == {1, 2}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_should_match_login_containing_copilot_substring() -> None:
|
|
271
|
+
pages_payload = json.dumps(
|
|
272
|
+
[
|
|
273
|
+
[
|
|
274
|
+
{
|
|
275
|
+
"id": 1,
|
|
276
|
+
"user": {"login": "copilot-pull-request-reviewer[bot]"},
|
|
277
|
+
"state": "CHANGES_REQUESTED",
|
|
278
|
+
"commit_id": "abc",
|
|
279
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
280
|
+
"body": "canonical bot login",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"id": 2,
|
|
284
|
+
"user": {"login": "internal-copilot-fork[bot]"},
|
|
285
|
+
"state": "CHANGES_REQUESTED",
|
|
286
|
+
"commit_id": "abc",
|
|
287
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
288
|
+
"body": "non-canonical login still matches",
|
|
289
|
+
},
|
|
290
|
+
]
|
|
291
|
+
]
|
|
292
|
+
)
|
|
293
|
+
with patch("subprocess.run") as mock_run:
|
|
294
|
+
mock_run.return_value = _completed(pages_payload)
|
|
295
|
+
all_reviews = fetch_copilot_reviews_module.fetch_copilot_reviews(
|
|
296
|
+
owner="acme", repo="widget", number=42
|
|
297
|
+
)
|
|
298
|
+
assert {each_review["review_id"] for each_review in all_reviews} == {1, 2}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_should_exclude_login_without_copilot_substring() -> None:
|
|
302
|
+
pages_payload = json.dumps(
|
|
303
|
+
[
|
|
304
|
+
[
|
|
305
|
+
{
|
|
306
|
+
"id": 1,
|
|
307
|
+
"user": {"login": "cursor[bot]"},
|
|
308
|
+
"state": "CHANGES_REQUESTED",
|
|
309
|
+
"commit_id": "abc",
|
|
310
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
311
|
+
"body": "wrong bot",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"id": 2,
|
|
315
|
+
"user": {"login": "dependabot[bot]"},
|
|
316
|
+
"state": "CHANGES_REQUESTED",
|
|
317
|
+
"commit_id": "abc",
|
|
318
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
319
|
+
"body": "also wrong",
|
|
320
|
+
},
|
|
321
|
+
]
|
|
322
|
+
]
|
|
323
|
+
)
|
|
324
|
+
with patch("subprocess.run") as mock_run:
|
|
325
|
+
mock_run.return_value = _completed(pages_payload)
|
|
326
|
+
all_reviews = fetch_copilot_reviews_module.fetch_copilot_reviews(
|
|
327
|
+
owner="acme", repo="widget", number=42
|
|
328
|
+
)
|
|
329
|
+
assert all_reviews == []
|
|
244
330
|
|
|
245
331
|
|
|
246
332
|
def test_should_raise_when_gh_subprocess_fails() -> None:
|