claude-dev-env 1.34.1 → 1.36.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 (148) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/agents/docs-agent.md +1 -1
  3. package/agents/project-docs-analyzer.md +0 -1
  4. package/agents/skill-to-agent-converter.md +0 -1
  5. package/bin/install.mjs +28 -8
  6. package/bin/install.test.mjs +9 -1
  7. package/commands/initialize.md +0 -1
  8. package/commands/readability-review.md +4 -4
  9. package/commands/review-plan.md +2 -4
  10. package/commands/stubcheck.md +1 -2
  11. package/docs/CODE_RULES.md +3 -0
  12. package/docs/agents-md-alignment-plan.md +123 -0
  13. package/hooks/blocking/code_rules_enforcer.py +686 -60
  14. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  15. package/hooks/blocking/test_code_rules_enforcer.py +273 -39
  16. package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
  17. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  18. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  19. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
  20. package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
  21. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
  22. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
  23. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  24. package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
  25. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
  26. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -13
  27. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
  28. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
  29. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  31. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  32. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  33. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  34. package/hooks/config/banned_identifiers_constants.py +24 -0
  35. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  36. package/hooks/config/hook_log_extractor_constants.py +1 -1
  37. package/hooks/config/pre_tool_use_stdin.py +48 -0
  38. package/hooks/config/setup_project_paths_constants.py +4 -0
  39. package/hooks/config/stuttering_check_config.py +14 -0
  40. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  41. package/hooks/config/sys_path_insert_constants.py +4 -0
  42. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  43. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  44. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  45. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  46. package/hooks/config/unused_module_import_constants.py +7 -0
  47. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  48. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  49. package/hooks/git-hooks/config.py +3 -3
  50. package/hooks/git-hooks/test_gate_utils.py +10 -10
  51. package/hooks/mypy.ini +2 -0
  52. package/package.json +1 -1
  53. package/rules/gh-paginate.md +125 -0
  54. package/skills/bugteam/CONSTRAINTS.md +12 -6
  55. package/skills/bugteam/PROMPTS.md +0 -39
  56. package/skills/bugteam/SKILL.md +93 -125
  57. package/skills/bugteam/SKILL_EVALS.md +25 -23
  58. package/skills/bugteam/reference/README.md +2 -0
  59. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  60. package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
  61. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  62. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  63. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  64. package/skills/bugteam/test_skill_additions.py +13 -4
  65. package/skills/bugteam/test_team_lifecycle.py +94 -0
  66. package/skills/findbugs/SKILL.md +3 -3
  67. package/skills/fixbugs/SKILL.md +4 -4
  68. package/skills/monitor-open-prs/SKILL.md +32 -2
  69. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  70. package/skills/pr-converge/SKILL.md +576 -95
  71. package/skills/pr-converge/scripts/README.md +145 -0
  72. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  73. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  74. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  75. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  76. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  77. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  78. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  79. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  80. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  81. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  82. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  83. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  84. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  85. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  86. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  87. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  88. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  89. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  90. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  91. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  92. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  93. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  94. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  95. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  96. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  97. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  98. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  99. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  100. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  101. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  102. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  103. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  104. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  105. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  106. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  107. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  108. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  109. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  110. package/skills/pr-converge/test_team_lifecycle.py +47 -0
  111. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  112. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  113. package/skills/qbug/SKILL.md +4 -4
  114. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  115. package/skills/resume-review/SKILL.md +261 -0
  116. package/agents/agent-writer.md +0 -157
  117. package/agents/config-centralizer.md +0 -686
  118. package/agents/config-extraction-agent.md +0 -225
  119. package/agents/doc-orchestrator.md +0 -47
  120. package/agents/docx-agent.md +0 -211
  121. package/agents/magic-value-eliminator-agent.md +0 -72
  122. package/agents/mandatory-agent-workflow-agent.md +0 -88
  123. package/agents/parallel-workflow-coordinator.md +0 -779
  124. package/agents/pdf-agent.md +0 -302
  125. package/agents/project-context-loader.md +0 -238
  126. package/agents/readability-review-agent.md +0 -76
  127. package/agents/refactoring-specialist.md +0 -69
  128. package/agents/right-sized-engineer.md +0 -129
  129. package/agents/session-continuity-manager.md +0 -53
  130. package/agents/stub-detector-agent.md +0 -140
  131. package/agents/tdd-test-writer.md +0 -62
  132. package/agents/test-data-builder.md +0 -68
  133. package/agents/tooling-builder.md +0 -78
  134. package/agents/validation-expert.md +0 -71
  135. package/agents/xlsx-agent.md +0 -169
  136. package/skills/bugteam/scripts/README.md +0 -58
  137. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  138. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  139. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  140. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  141. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  142. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  143. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  144. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  145. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  146. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  147. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  148. /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
@@ -0,0 +1,372 @@
1
+ """Tests for fetch_copilot_inline_comments.
2
+
3
+ Covers:
4
+ - gh command uses --paginate --slurp on the comments endpoint
5
+ - only copilot-pull-request-reviewer[bot] inline comments are returned
6
+ - comments not anchored to the requested commit are filtered out
7
+ - comments on the same commit but from an older Copilot review are filtered out
8
+ - multi-page responses are flattened correctly
9
+ - subprocess errors propagate
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ import json
16
+ import subprocess
17
+ from pathlib import Path
18
+ from types import ModuleType
19
+ from unittest.mock import MagicMock, patch
20
+
21
+ import pytest
22
+
23
+
24
+ def _load_module() -> ModuleType:
25
+ module_path = Path(__file__).parent / "fetch_copilot_inline_comments.py"
26
+ spec = importlib.util.spec_from_file_location(
27
+ "fetch_copilot_inline_comments", module_path
28
+ )
29
+ assert spec is not None
30
+ assert spec.loader is not None
31
+ module = importlib.util.module_from_spec(spec)
32
+ spec.loader.exec_module(module)
33
+ return module
34
+
35
+
36
+ fetch_copilot_inline_comments_module = _load_module()
37
+
38
+
39
+ def _completed(stdout: str) -> subprocess.CompletedProcess:
40
+ process = MagicMock(spec=subprocess.CompletedProcess)
41
+ process.stdout = stdout
42
+ process.returncode = 0
43
+ return process
44
+
45
+
46
+ def _default_review_for_head(*, commit: str, review_id: int) -> list[dict]:
47
+ return [
48
+ {
49
+ "review_id": review_id,
50
+ "commit_id": commit,
51
+ "submitted_at": "2026-01-01T00:00:00Z",
52
+ "state": "CHANGES_REQUESTED",
53
+ "body": "Please address the inline notes.",
54
+ "classification": "dirty",
55
+ }
56
+ ]
57
+
58
+
59
+ def test_should_invoke_gh_with_paginate_slurp_against_comments_endpoint() -> None:
60
+ pages_payload = json.dumps([[]])
61
+ with (
62
+ patch.object(
63
+ fetch_copilot_inline_comments_module,
64
+ "fetch_copilot_reviews",
65
+ return_value=_default_review_for_head(commit="abc123", review_id=1),
66
+ ),
67
+ patch("subprocess.run") as mock_run,
68
+ ):
69
+ mock_run.return_value = _completed(pages_payload)
70
+ fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
71
+ owner="acme", repo="widget", number=42, current_head="abc123"
72
+ )
73
+ invoked_argv = mock_run.call_args[0][0]
74
+ assert invoked_argv[0] == "gh"
75
+ assert invoked_argv[1] == "api"
76
+ assert "repos/acme/widget/pulls/42/comments?per_page=100" in invoked_argv[2]
77
+ assert "--paginate" in invoked_argv
78
+ assert "--slurp" in invoked_argv
79
+
80
+
81
+ def test_should_filter_to_copilot_reviewer_only() -> None:
82
+ pages_payload = json.dumps(
83
+ [
84
+ [
85
+ {
86
+ "id": 100,
87
+ "user": {"login": "cursor[bot]"},
88
+ "commit_id": "abc123",
89
+ "pull_request_review_id": 1,
90
+ "body": "bugbot finding",
91
+ "path": "x.py",
92
+ "line": 5,
93
+ },
94
+ {
95
+ "id": 101,
96
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
97
+ "commit_id": "abc123",
98
+ "pull_request_review_id": 1,
99
+ "body": "copilot finding",
100
+ "path": "x.py",
101
+ "line": 6,
102
+ },
103
+ ]
104
+ ]
105
+ )
106
+ with (
107
+ patch.object(
108
+ fetch_copilot_inline_comments_module,
109
+ "fetch_copilot_reviews",
110
+ return_value=_default_review_for_head(commit="abc123", review_id=1),
111
+ ),
112
+ patch("subprocess.run") as mock_run,
113
+ ):
114
+ mock_run.return_value = _completed(pages_payload)
115
+ all_inline_comments = (
116
+ fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
117
+ owner="acme", repo="widget", number=42, current_head="abc123"
118
+ )
119
+ )
120
+ assert len(all_inline_comments) == 1
121
+ assert all_inline_comments[0]["comment_id"] == 101
122
+
123
+
124
+ def test_should_filter_out_comments_not_on_current_head() -> None:
125
+ pages_payload = json.dumps(
126
+ [
127
+ [
128
+ {
129
+ "id": 200,
130
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
131
+ "commit_id": "old_sha",
132
+ "pull_request_review_id": 1,
133
+ "body": "stale finding",
134
+ "path": "x.py",
135
+ "line": 5,
136
+ },
137
+ {
138
+ "id": 201,
139
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
140
+ "commit_id": "current_sha",
141
+ "pull_request_review_id": 2,
142
+ "body": "fresh finding",
143
+ "path": "x.py",
144
+ "line": 6,
145
+ },
146
+ ]
147
+ ]
148
+ )
149
+ with (
150
+ patch.object(
151
+ fetch_copilot_inline_comments_module,
152
+ "fetch_copilot_reviews",
153
+ return_value=_default_review_for_head(commit="current_sha", review_id=2),
154
+ ),
155
+ patch("subprocess.run") as mock_run,
156
+ ):
157
+ mock_run.return_value = _completed(pages_payload)
158
+ all_inline_comments = (
159
+ fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
160
+ owner="acme", repo="widget", number=42, current_head="current_sha"
161
+ )
162
+ )
163
+ assert len(all_inline_comments) == 1
164
+ assert all_inline_comments[0]["comment_id"] == 201
165
+
166
+
167
+ def test_should_ignore_inline_comments_from_older_copilot_review_on_same_commit() -> (
168
+ None
169
+ ):
170
+ pages_payload = json.dumps(
171
+ [
172
+ [
173
+ {
174
+ "id": 300,
175
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
176
+ "commit_id": "same_sha",
177
+ "pull_request_review_id": 10,
178
+ "body": "stale dirty thread",
179
+ "path": "x.py",
180
+ "line": 1,
181
+ },
182
+ {
183
+ "id": 301,
184
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
185
+ "commit_id": "same_sha",
186
+ "pull_request_review_id": 11,
187
+ "body": "current clean thread",
188
+ "path": "x.py",
189
+ "line": 2,
190
+ },
191
+ ]
192
+ ]
193
+ )
194
+ reviews_newest_first = [
195
+ {
196
+ "review_id": 11,
197
+ "commit_id": "same_sha",
198
+ "submitted_at": "2026-01-02T00:00:00Z",
199
+ "state": "APPROVED",
200
+ "body": "lgtm",
201
+ "classification": "clean",
202
+ },
203
+ {
204
+ "review_id": 10,
205
+ "commit_id": "same_sha",
206
+ "submitted_at": "2026-01-01T00:00:00Z",
207
+ "state": "CHANGES_REQUESTED",
208
+ "body": "fix the thing",
209
+ "classification": "dirty",
210
+ },
211
+ ]
212
+ with (
213
+ patch.object(
214
+ fetch_copilot_inline_comments_module,
215
+ "fetch_copilot_reviews",
216
+ return_value=reviews_newest_first,
217
+ ),
218
+ patch("subprocess.run") as mock_run,
219
+ ):
220
+ mock_run.return_value = _completed(pages_payload)
221
+ all_inline_comments = (
222
+ fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
223
+ owner="acme", repo="widget", number=42, current_head="same_sha"
224
+ )
225
+ )
226
+ assert [each_comment["comment_id"] for each_comment in all_inline_comments] == [301]
227
+
228
+
229
+ def test_should_return_empty_when_no_copilot_review_exists_for_commit() -> None:
230
+ with (
231
+ patch.object(
232
+ fetch_copilot_inline_comments_module,
233
+ "fetch_copilot_reviews",
234
+ return_value=[
235
+ {
236
+ "review_id": 1,
237
+ "commit_id": "other_sha",
238
+ "submitted_at": "2026-01-01T00:00:00Z",
239
+ "state": "APPROVED",
240
+ "body": "",
241
+ "classification": "clean",
242
+ }
243
+ ],
244
+ ),
245
+ patch("subprocess.run") as mock_run,
246
+ ):
247
+ all_inline_comments = (
248
+ fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
249
+ owner="acme", repo="widget", number=42, current_head="missing_sha"
250
+ )
251
+ )
252
+ assert all_inline_comments == []
253
+ mock_run.assert_not_called()
254
+
255
+
256
+ def test_should_flatten_across_pages() -> None:
257
+ pages_payload = json.dumps(
258
+ [
259
+ [
260
+ {
261
+ "id": 1,
262
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
263
+ "commit_id": "abc",
264
+ "pull_request_review_id": 9,
265
+ "body": "a",
266
+ "path": "f.py",
267
+ "line": 1,
268
+ }
269
+ ],
270
+ [
271
+ {
272
+ "id": 2,
273
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
274
+ "commit_id": "abc",
275
+ "pull_request_review_id": 9,
276
+ "body": "b",
277
+ "path": "f.py",
278
+ "line": 2,
279
+ },
280
+ {
281
+ "id": 3,
282
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
283
+ "commit_id": "abc",
284
+ "pull_request_review_id": 9,
285
+ "body": "c",
286
+ "path": "f.py",
287
+ "line": 3,
288
+ },
289
+ ],
290
+ ]
291
+ )
292
+ with (
293
+ patch.object(
294
+ fetch_copilot_inline_comments_module,
295
+ "fetch_copilot_reviews",
296
+ return_value=_default_review_for_head(commit="abc", review_id=9),
297
+ ),
298
+ patch("subprocess.run") as mock_run,
299
+ ):
300
+ mock_run.return_value = _completed(pages_payload)
301
+ all_inline_comments = (
302
+ fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
303
+ owner="acme", repo="widget", number=42, current_head="abc"
304
+ )
305
+ )
306
+ assert [each_comment["comment_id"] for each_comment in all_inline_comments] == [
307
+ 1,
308
+ 2,
309
+ 3,
310
+ ]
311
+
312
+
313
+ def test_should_reference_copilot_login_constant_directly_without_local_alias() -> None:
314
+ source_text = (
315
+ Path(__file__).resolve().parent / "fetch_copilot_inline_comments.py"
316
+ ).read_text(encoding="utf-8")
317
+ assert "copilot_reviewer_login = COPILOT_REVIEWER_LOGIN" not in source_text
318
+ assert "COPILOT_REVIEWER_LOGIN" in source_text
319
+
320
+
321
+ def test_should_raise_when_gh_subprocess_fails() -> None:
322
+ failure = subprocess.CalledProcessError(
323
+ returncode=1, cmd=["gh"], stderr="auth failure"
324
+ )
325
+ with (
326
+ patch.object(
327
+ fetch_copilot_inline_comments_module,
328
+ "fetch_copilot_reviews",
329
+ return_value=_default_review_for_head(commit="abc", review_id=1),
330
+ ),
331
+ patch("subprocess.run", side_effect=failure),
332
+ ):
333
+ with pytest.raises(subprocess.CalledProcessError):
334
+ fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
335
+ owner="acme", repo="widget", number=42, current_head="abc"
336
+ )
337
+
338
+
339
+ def test_should_return_entries_whose_keys_are_strings() -> None:
340
+ pages_payload = json.dumps(
341
+ [
342
+ [
343
+ {
344
+ "id": 101,
345
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
346
+ "commit_id": "abc123",
347
+ "pull_request_review_id": 1,
348
+ "body": "copilot finding",
349
+ "path": "x.py",
350
+ "line": 6,
351
+ }
352
+ ]
353
+ ]
354
+ )
355
+ with (
356
+ patch.object(
357
+ fetch_copilot_inline_comments_module,
358
+ "fetch_copilot_reviews",
359
+ return_value=_default_review_for_head(commit="abc123", review_id=1),
360
+ ),
361
+ patch("subprocess.run") as mock_run,
362
+ ):
363
+ mock_run.return_value = _completed(pages_payload)
364
+ all_inline_comments = (
365
+ fetch_copilot_inline_comments_module.fetch_copilot_inline_comments(
366
+ owner="acme", repo="widget", number=42, current_head="abc123"
367
+ )
368
+ )
369
+ assert len(all_inline_comments) == 1
370
+ first_comment_entry = all_inline_comments[0]
371
+ assert isinstance(first_comment_entry, dict)
372
+ assert all(isinstance(each_key, str) for each_key in first_comment_entry.keys())
@@ -0,0 +1,280 @@
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 / "fetch_copilot_reviews.py"
210
+ ).read_text(encoding="utf-8")
211
+ assert "ALL_COPILOT_DIRTY_REVIEW_STATES" in source_text
212
+ assert "in 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_reference_copilot_login_constant_directly_without_local_alias() -> None:
239
+ source_text = (
240
+ Path(__file__).resolve().parent / "fetch_copilot_reviews.py"
241
+ ).read_text(encoding="utf-8")
242
+ assert "copilot_reviewer_login = COPILOT_REVIEWER_LOGIN" not in source_text
243
+ assert "COPILOT_REVIEWER_LOGIN" in source_text
244
+
245
+
246
+ def test_should_raise_when_gh_subprocess_fails() -> None:
247
+ failure = subprocess.CalledProcessError(
248
+ returncode=1, cmd=["gh"], stderr="auth failure"
249
+ )
250
+ with patch("subprocess.run", side_effect=failure):
251
+ with pytest.raises(subprocess.CalledProcessError):
252
+ fetch_copilot_reviews_module.fetch_copilot_reviews(
253
+ owner="acme", repo="widget", number=42
254
+ )
255
+
256
+
257
+ def test_should_return_entries_whose_keys_are_strings() -> None:
258
+ pages_payload = json.dumps(
259
+ [
260
+ [
261
+ {
262
+ "id": 1,
263
+ "user": {"login": "copilot-pull-request-reviewer[bot]"},
264
+ "state": "APPROVED",
265
+ "commit_id": "abc",
266
+ "submitted_at": "2026-01-01T00:00:00Z",
267
+ "body": "looks good",
268
+ }
269
+ ]
270
+ ]
271
+ )
272
+ with patch("subprocess.run") as mock_run:
273
+ mock_run.return_value = _completed(pages_payload)
274
+ all_reviews = fetch_copilot_reviews_module.fetch_copilot_reviews(
275
+ owner="acme", repo="widget", number=42
276
+ )
277
+ assert len(all_reviews) == 1
278
+ first_review_entry = all_reviews[0]
279
+ assert isinstance(first_review_entry, dict)
280
+ assert all(isinstance(each_key, str) for each_key in first_review_entry.keys())