claude-dev-env 1.37.1 → 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/no-historical-clutter.md +31 -10
- 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 +60 -31
- package/skills/bugteam/SKILL.md +47 -47
- package/skills/bugteam/SKILL_EVALS.md +8 -8
- 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 +28 -30
- package/skills/pr-converge/reference/examples.md +4 -4
- package/skills/pr-converge/reference/fix-protocol.md +6 -8
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -10
- package/skills/pr-converge/reference/per-tick.md +18 -33
- 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 -134
- 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 -155
- package/skills/pr-converge/scripts/trigger_bugbot.py +0 -77
- package/skills/pr-converge/scripts/view_pr_context.py +0 -78
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +0 -376
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
"""Tests for fetch_copilot_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 copilot-pull-request-reviewer[bot] reviews are returned
|
|
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_copilot_reviews.py"
|
|
27
|
-
spec = importlib.util.spec_from_file_location("fetch_copilot_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_copilot_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_copilot_reviews_module.fetch_copilot_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_copilot_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": "copilot-pull-request-reviewer[bot]"},
|
|
75
|
-
"state": "CHANGES_REQUESTED",
|
|
76
|
-
"commit_id": "abc",
|
|
77
|
-
"submitted_at": "2026-01-02T00:00:00Z",
|
|
78
|
-
"body": "copilot 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_copilot_reviews_module.fetch_copilot_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": "copilot-pull-request-reviewer[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": "copilot-pull-request-reviewer[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": "copilot-pull-request-reviewer[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_copilot_reviews_module.fetch_copilot_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": "copilot-pull-request-reviewer[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_copilot_reviews_module.fetch_copilot_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": "copilot-pull-request-reviewer[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_copilot_reviews_module.fetch_copilot_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": "copilot-pull-request-reviewer[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_copilot_reviews_module.fetch_copilot_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_copilot_dirty_review_states_tuple() -> None:
|
|
208
|
-
source_text = (
|
|
209
|
-
Path(__file__).resolve().parent / "reviewer_specs.py"
|
|
210
|
-
).read_text(encoding="utf-8")
|
|
211
|
-
assert "ALL_COPILOT_DIRTY_REVIEW_STATES" in source_text
|
|
212
|
-
assert "all_dirty_states=ALL_COPILOT_DIRTY_REVIEW_STATES" in source_text
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def test_should_classify_clean_review_when_state_is_approved() -> None:
|
|
216
|
-
pages_payload = json.dumps(
|
|
217
|
-
[
|
|
218
|
-
[
|
|
219
|
-
{
|
|
220
|
-
"id": 1,
|
|
221
|
-
"user": {"login": "copilot-pull-request-reviewer[bot]"},
|
|
222
|
-
"state": "APPROVED",
|
|
223
|
-
"commit_id": "abc",
|
|
224
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
225
|
-
"body": "looks good",
|
|
226
|
-
}
|
|
227
|
-
]
|
|
228
|
-
]
|
|
229
|
-
)
|
|
230
|
-
with patch("subprocess.run") as mock_run:
|
|
231
|
-
mock_run.return_value = _completed(pages_payload)
|
|
232
|
-
all_reviews = fetch_copilot_reviews_module.fetch_copilot_reviews(
|
|
233
|
-
owner="acme", repo="widget", number=42
|
|
234
|
-
)
|
|
235
|
-
assert all_reviews[0]["classification"] == "clean"
|
|
236
|
-
|
|
237
|
-
|
|
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 == []
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def test_should_raise_when_gh_subprocess_fails() -> None:
|
|
333
|
-
failure = subprocess.CalledProcessError(
|
|
334
|
-
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
335
|
-
)
|
|
336
|
-
with patch("subprocess.run", side_effect=failure):
|
|
337
|
-
with pytest.raises(subprocess.CalledProcessError):
|
|
338
|
-
fetch_copilot_reviews_module.fetch_copilot_reviews(
|
|
339
|
-
owner="acme", repo="widget", number=42
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def test_should_return_entries_whose_keys_are_strings() -> None:
|
|
344
|
-
pages_payload = json.dumps(
|
|
345
|
-
[
|
|
346
|
-
[
|
|
347
|
-
{
|
|
348
|
-
"id": 1,
|
|
349
|
-
"user": {"login": "copilot-pull-request-reviewer[bot]"},
|
|
350
|
-
"state": "APPROVED",
|
|
351
|
-
"commit_id": "abc",
|
|
352
|
-
"submitted_at": "2026-01-01T00:00:00Z",
|
|
353
|
-
"body": "looks good",
|
|
354
|
-
}
|
|
355
|
-
]
|
|
356
|
-
]
|
|
357
|
-
)
|
|
358
|
-
with patch("subprocess.run") as mock_run:
|
|
359
|
-
mock_run.return_value = _completed(pages_payload)
|
|
360
|
-
all_reviews = fetch_copilot_reviews_module.fetch_copilot_reviews(
|
|
361
|
-
owner="acme", repo="widget", number=42
|
|
362
|
-
)
|
|
363
|
-
assert len(all_reviews) == 1
|
|
364
|
-
first_review_entry = all_reviews[0]
|
|
365
|
-
assert isinstance(first_review_entry, dict)
|
|
366
|
-
assert all(isinstance(each_key, str) for each_key in first_review_entry.keys())
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"""Tests for mark_pr_ready.
|
|
2
|
-
|
|
3
|
-
Covers:
|
|
4
|
-
- gh pr ready is invoked with the PR number and --repo flag
|
|
5
|
-
- subprocess errors propagate
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import importlib.util
|
|
11
|
-
import subprocess
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from types import ModuleType
|
|
14
|
-
from unittest.mock import MagicMock, patch
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _load_module() -> ModuleType:
|
|
20
|
-
module_path = Path(__file__).parent / "mark_pr_ready.py"
|
|
21
|
-
spec = importlib.util.spec_from_file_location("mark_pr_ready", module_path)
|
|
22
|
-
assert spec is not None
|
|
23
|
-
assert spec.loader is not None
|
|
24
|
-
module = importlib.util.module_from_spec(spec)
|
|
25
|
-
spec.loader.exec_module(module)
|
|
26
|
-
return module
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
mark_pr_ready_module = _load_module()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _completed(stdout: str) -> subprocess.CompletedProcess:
|
|
33
|
-
process = MagicMock(spec=subprocess.CompletedProcess)
|
|
34
|
-
process.stdout = stdout
|
|
35
|
-
process.returncode = 0
|
|
36
|
-
return process
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_should_invoke_gh_pr_ready_with_number_and_repo() -> None:
|
|
40
|
-
with patch("subprocess.run") as mock_run:
|
|
41
|
-
mock_run.return_value = _completed('Pull request "#42" is marked as ready')
|
|
42
|
-
mark_pr_ready_module.mark_pr_ready(owner="acme", repo="widget", number=42)
|
|
43
|
-
invoked_argv = mock_run.call_args[0][0]
|
|
44
|
-
assert invoked_argv[0:3] == ["gh", "pr", "ready"]
|
|
45
|
-
assert "42" in invoked_argv
|
|
46
|
-
assert "--repo" in invoked_argv
|
|
47
|
-
assert "acme/widget" in invoked_argv
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_should_raise_when_gh_subprocess_fails() -> None:
|
|
51
|
-
failure = subprocess.CalledProcessError(
|
|
52
|
-
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
53
|
-
)
|
|
54
|
-
with patch("subprocess.run", side_effect=failure):
|
|
55
|
-
with pytest.raises(subprocess.CalledProcessError):
|
|
56
|
-
mark_pr_ready_module.mark_pr_ready(owner="acme", repo="widget", number=42)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_should_render_repo_arg_via_named_template_constant() -> None:
|
|
60
|
-
with patch("subprocess.run") as mock_run:
|
|
61
|
-
mock_run.return_value = _completed("ok\n")
|
|
62
|
-
mark_pr_ready_module.mark_pr_ready(owner="acme", repo="widget", number=42)
|
|
63
|
-
invoked_argv = mock_run.call_args[0][0]
|
|
64
|
-
expected_repo_arg = mark_pr_ready_module.GH_REPO_ARG_TEMPLATE.format(
|
|
65
|
-
owner="acme", repo="widget"
|
|
66
|
-
)
|
|
67
|
-
assert expected_repo_arg == "acme/widget"
|
|
68
|
-
repo_flag_index = invoked_argv.index("--repo")
|
|
69
|
-
assert invoked_argv[repo_flag_index + 1] == expected_repo_arg
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
"""Tests for post-bugbot-run PowerShell helpers.
|
|
2
|
-
|
|
3
|
-
Covers Resolve-InvocationMode / Build-GhArgumentList for URL, owner/repo#N, and
|
|
4
|
-
-Repository/-Number forms (Windows Bugbot re-trigger argv construction).
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
import pytest
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _run_powershell(*, expression: str) -> str:
|
|
19
|
-
helpers = Path(__file__).resolve().parent / "post-bugbot-run.helpers.ps1"
|
|
20
|
-
command = (
|
|
21
|
-
f". '{helpers}'; "
|
|
22
|
-
+ expression
|
|
23
|
-
+ " | ConvertTo-Json -Compress -Depth 5"
|
|
24
|
-
)
|
|
25
|
-
completed = subprocess.run(
|
|
26
|
-
["pwsh", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
27
|
-
capture_output=True,
|
|
28
|
-
text=True,
|
|
29
|
-
encoding="utf-8",
|
|
30
|
-
errors="replace",
|
|
31
|
-
check=False,
|
|
32
|
-
)
|
|
33
|
-
if completed.returncode != 0:
|
|
34
|
-
raise AssertionError(
|
|
35
|
-
f"pwsh failed ({completed.returncode}): stderr={completed.stderr!r} stdout={completed.stdout!r}"
|
|
36
|
-
)
|
|
37
|
-
return completed.stdout.strip()
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _argv_for(*, pull: str, repository: str, number: int, body: str) -> list[str]:
|
|
41
|
-
pull_esc = pull.replace("'", "''")
|
|
42
|
-
repository_esc = repository.replace("'", "''")
|
|
43
|
-
body_esc = body.replace("'", "''")
|
|
44
|
-
expression = (
|
|
45
|
-
f"$i = Resolve-InvocationMode -PullRequestInput '{pull_esc}' "
|
|
46
|
-
f"-RepositoryInput '{repository_esc}' -NumberInput {int(number)}; "
|
|
47
|
-
f"@(Build-GhArgumentList -Invocation $i -BodyFilePath '{body_esc}')"
|
|
48
|
-
)
|
|
49
|
-
raw = _run_powershell(expression=expression)
|
|
50
|
-
return json.loads(raw)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_should_build_arguments_for_https_pull_url() -> None:
|
|
54
|
-
argv = _argv_for(
|
|
55
|
-
pull="https://github.com/acme/widget/pull/42",
|
|
56
|
-
repository="",
|
|
57
|
-
number=0,
|
|
58
|
-
body=r"C:\\temp\\body.md",
|
|
59
|
-
)
|
|
60
|
-
assert argv == [
|
|
61
|
-
"pr",
|
|
62
|
-
"comment",
|
|
63
|
-
"https://github.com/acme/widget/pull/42",
|
|
64
|
-
"--body-file",
|
|
65
|
-
r"C:\\temp\\body.md",
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_should_build_arguments_for_owner_repo_hash_form() -> None:
|
|
70
|
-
argv = _argv_for(
|
|
71
|
-
pull="acme/widget#7",
|
|
72
|
-
repository="",
|
|
73
|
-
number=0,
|
|
74
|
-
body=r"D:\\x\\f.md",
|
|
75
|
-
)
|
|
76
|
-
assert argv == ["pr", "comment", "7", "-R", "acme/widget", "--body-file", r"D:\\x\\f.md"]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_should_build_arguments_for_repository_and_number_parameters() -> None:
|
|
80
|
-
argv = _argv_for(
|
|
81
|
-
pull="",
|
|
82
|
-
repository="jl-cmd/claude-code-config",
|
|
83
|
-
number=331,
|
|
84
|
-
body=r"E:\\y\\z.md",
|
|
85
|
-
)
|
|
86
|
-
assert argv == [
|
|
87
|
-
"pr",
|
|
88
|
-
"comment",
|
|
89
|
-
"331",
|
|
90
|
-
"-R",
|
|
91
|
-
"jl-cmd/claude-code-config",
|
|
92
|
-
"--body-file",
|
|
93
|
-
r"E:\\y\\z.md",
|
|
94
|
-
]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_should_fail_when_number_without_repository() -> None:
|
|
98
|
-
helpers = Path(__file__).resolve().parent / "post-bugbot-run.helpers.ps1"
|
|
99
|
-
command = (
|
|
100
|
-
f". '{helpers}'; "
|
|
101
|
-
"try { Resolve-InvocationMode -PullRequestInput '' -RepositoryInput '' -NumberInput 3 } "
|
|
102
|
-
"catch { $_.Exception.Message }"
|
|
103
|
-
)
|
|
104
|
-
completed = subprocess.run(
|
|
105
|
-
["pwsh", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
106
|
-
capture_output=True,
|
|
107
|
-
text=True,
|
|
108
|
-
encoding="utf-8",
|
|
109
|
-
errors="replace",
|
|
110
|
-
check=False,
|
|
111
|
-
)
|
|
112
|
-
assert completed.returncode == 0
|
|
113
|
-
message = completed.stdout.strip()
|
|
114
|
-
assert "Repository" in message
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def test_should_reject_pull_url_with_trailing_junk() -> None:
|
|
118
|
-
helpers = Path(__file__).resolve().parent / "post-bugbot-run.helpers.ps1"
|
|
119
|
-
pull = "https://github.com/acme/widget/pull/42extra"
|
|
120
|
-
command = (
|
|
121
|
-
f". '{helpers}'; "
|
|
122
|
-
f"$i = Resolve-InvocationMode -PullRequestInput '{pull}' -RepositoryInput '' -NumberInput 0; "
|
|
123
|
-
r"try { Build-GhArgumentList -Invocation $i -BodyFilePath 'C:\\t\\b.md' } "
|
|
124
|
-
"catch { $_.Exception.Message }"
|
|
125
|
-
)
|
|
126
|
-
completed = subprocess.run(
|
|
127
|
-
["pwsh", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
128
|
-
capture_output=True,
|
|
129
|
-
text=True,
|
|
130
|
-
encoding="utf-8",
|
|
131
|
-
errors="replace",
|
|
132
|
-
check=False,
|
|
133
|
-
)
|
|
134
|
-
assert completed.returncode == 0
|
|
135
|
-
assert "Unrecognized PullRequest" in completed.stdout
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def test_should_fail_for_unrecognized_pull_string() -> None:
|
|
139
|
-
helpers = Path(__file__).resolve().parent / "post-bugbot-run.helpers.ps1"
|
|
140
|
-
pull = "not-a-valid-pr-reference"
|
|
141
|
-
command = (
|
|
142
|
-
f". '{helpers}'; "
|
|
143
|
-
f"$i = Resolve-InvocationMode -PullRequestInput '{pull}' -RepositoryInput '' -NumberInput 0; "
|
|
144
|
-
r"try { Build-GhArgumentList -Invocation $i -BodyFilePath 'C:\\t\\b.md' } "
|
|
145
|
-
"catch { $_.Exception.Message }"
|
|
146
|
-
)
|
|
147
|
-
completed = subprocess.run(
|
|
148
|
-
["pwsh", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
149
|
-
capture_output=True,
|
|
150
|
-
text=True,
|
|
151
|
-
encoding="utf-8",
|
|
152
|
-
errors="replace",
|
|
153
|
-
check=False,
|
|
154
|
-
)
|
|
155
|
-
assert completed.returncode == 0
|
|
156
|
-
assert "Unrecognized PullRequest" in completed.stdout
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def test_full_script_removes_temp_files_when_gh_stub_succeeds(tmp_path: Path) -> None:
|
|
160
|
-
scripts_dir = Path(__file__).resolve().parent
|
|
161
|
-
script_path = scripts_dir / "post-bugbot-run.ps1"
|
|
162
|
-
stub_bin_dir = tmp_path / "gh_stub_bin"
|
|
163
|
-
stub_bin_dir.mkdir()
|
|
164
|
-
gh_cmd = stub_bin_dir / "gh.cmd"
|
|
165
|
-
gh_cmd.write_text("@echo off\r\nexit /b 0\r\n", encoding="utf-8")
|
|
166
|
-
env = dict(os.environ)
|
|
167
|
-
env["PATH"] = str(stub_bin_dir) + os.pathsep + env.get("PATH", "")
|
|
168
|
-
completed = subprocess.run(
|
|
169
|
-
[
|
|
170
|
-
"pwsh",
|
|
171
|
-
"-NoProfile",
|
|
172
|
-
"-NonInteractive",
|
|
173
|
-
"-ExecutionPolicy",
|
|
174
|
-
"Bypass",
|
|
175
|
-
"-File",
|
|
176
|
-
str(script_path),
|
|
177
|
-
"acme/widget#9",
|
|
178
|
-
],
|
|
179
|
-
capture_output=True,
|
|
180
|
-
text=True,
|
|
181
|
-
encoding="utf-8",
|
|
182
|
-
errors="replace",
|
|
183
|
-
env=env,
|
|
184
|
-
check=False,
|
|
185
|
-
)
|
|
186
|
-
assert completed.returncode == 0, (completed.stdout, completed.stderr)
|
|
187
|
-
|
|
188
|
-
def test_post_bugbot_run_script_finally_removes_temp_paths() -> None:
|
|
189
|
-
script_text = (Path(__file__).resolve().parent / "post-bugbot-run.ps1").read_text(
|
|
190
|
-
encoding="utf-8"
|
|
191
|
-
)
|
|
192
|
-
assert "finally" in script_text
|
|
193
|
-
assert "Remove-Item" in script_text
|
|
194
|
-
assert "body_file_path" in script_text
|
|
195
|
-
assert "scratch_temp_path" in script_text
|