claude-dev-env 1.35.0 → 1.36.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/agents/clean-coder.md +109 -1
- package/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -1
- package/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +451 -39
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +182 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +191 -0
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -0
- package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
- package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
- package/hooks/blocking/windows_rmtree_blocker.py +23 -6
- package/hooks/config/banned_identifiers_constants.py +24 -0
- package/hooks/config/hardcoded_user_path_constants.py +12 -0
- package/hooks/config/hook_log_extractor_constants.py +1 -1
- package/hooks/config/pre_tool_use_stdin.py +48 -0
- package/hooks/config/setup_project_paths_constants.py +4 -0
- package/hooks/config/stuttering_check_config.py +14 -0
- package/hooks/config/stuttering_import_binding_constants.py +11 -0
- package/hooks/config/sys_path_insert_constants.py +4 -0
- package/hooks/config/test_banned_identifiers_constants.py +48 -0
- package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
- package/hooks/config/test_hook_log_extractor_constants.py +3 -3
- package/hooks/config/test_pre_tool_use_stdin.py +80 -0
- package/hooks/config/unused_module_import_constants.py +7 -0
- package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
- package/hooks/git-hooks/config.py +3 -3
- package/hooks/git-hooks/test_gate_utils.py +10 -10
- package/hooks/mypy.ini +2 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +125 -0
- package/skills/bugteam/CONSTRAINTS.md +12 -6
- package/skills/bugteam/SKILL.md +364 -154
- package/skills/bugteam/SKILL_EVALS.md +25 -23
- package/skills/bugteam/reference/README.md +2 -0
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
- package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +103 -0
- package/skills/findbugs/SKILL.md +3 -3
- package/skills/fixbugs/SKILL.md +4 -4
- package/skills/monitor-open-prs/SKILL.md +32 -2
- package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
- package/skills/pr-converge/SKILL.md +1206 -131
- package/skills/pr-converge/scripts/README.md +145 -0
- package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
- package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
- package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
- package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
- package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
- package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
- package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
- package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
- package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
- package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
- package/skills/pr-converge/scripts/view_pr_context.py +47 -0
- package/skills/pr-converge/test_team_lifecycle.py +56 -0
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
- package/skills/qbug/SKILL.md +4 -4
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
- package/skills/resume-review/SKILL.md +261 -0
- package/skills/bugteam/scripts/README.md +0 -58
- package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
- package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
- package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
- package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
- /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Tests for fetch_bugbot_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 cursor[bot] reviews are returned
|
|
7
|
+
- reviews are sorted newest-first by submitted_at
|
|
8
|
+
- review bodies matching the bugbot dirty-pattern are classified "dirty"
|
|
9
|
+
- review bodies without the pattern are classified "clean"
|
|
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_bugbot_reviews.py"
|
|
27
|
+
spec = importlib.util.spec_from_file_location("fetch_bugbot_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_bugbot_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_bugbot_reviews_module.fetch_bugbot_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_cursor_bot_only() -> None:
|
|
61
|
+
pages_payload = json.dumps(
|
|
62
|
+
[
|
|
63
|
+
[
|
|
64
|
+
{
|
|
65
|
+
"id": 1,
|
|
66
|
+
"user": {"login": "copilot-pull-request-reviewer[bot]"},
|
|
67
|
+
"commit_id": "abc",
|
|
68
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
69
|
+
"body": "copilot stuff",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": 2,
|
|
73
|
+
"user": {"login": "cursor[bot]"},
|
|
74
|
+
"commit_id": "abc",
|
|
75
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
76
|
+
"body": "Cursor Bugbot has reviewed your changes and found 1 potential issue.",
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
with patch("subprocess.run") as mock_run:
|
|
82
|
+
mock_run.return_value = _completed(pages_payload)
|
|
83
|
+
all_reviews = fetch_bugbot_reviews_module.fetch_bugbot_reviews(
|
|
84
|
+
owner="acme", repo="widget", number=42
|
|
85
|
+
)
|
|
86
|
+
assert len(all_reviews) == 1
|
|
87
|
+
assert all_reviews[0]["review_id"] == 2
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_should_return_reviews_newest_first_across_pages() -> None:
|
|
91
|
+
pages_payload = json.dumps(
|
|
92
|
+
[
|
|
93
|
+
[
|
|
94
|
+
{
|
|
95
|
+
"id": 10,
|
|
96
|
+
"user": {"login": "cursor[bot]"},
|
|
97
|
+
"commit_id": "old",
|
|
98
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
99
|
+
"body": "Bugbot reviewed your changes and found no new issues!",
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
{
|
|
104
|
+
"id": 11,
|
|
105
|
+
"user": {"login": "cursor[bot]"},
|
|
106
|
+
"commit_id": "new",
|
|
107
|
+
"submitted_at": "2026-01-03T00:00:00Z",
|
|
108
|
+
"body": "Cursor Bugbot has reviewed your changes and found 2 potential issues.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": 12,
|
|
112
|
+
"user": {"login": "cursor[bot]"},
|
|
113
|
+
"commit_id": "mid",
|
|
114
|
+
"submitted_at": "2026-01-02T00:00:00Z",
|
|
115
|
+
"body": "Bugbot reviewed your changes and found no new issues!",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
with patch("subprocess.run") as mock_run:
|
|
121
|
+
mock_run.return_value = _completed(pages_payload)
|
|
122
|
+
all_reviews = fetch_bugbot_reviews_module.fetch_bugbot_reviews(
|
|
123
|
+
owner="acme", repo="widget", number=42
|
|
124
|
+
)
|
|
125
|
+
submitted_at_sequence = [each_review["submitted_at"] for each_review in all_reviews]
|
|
126
|
+
assert submitted_at_sequence == [
|
|
127
|
+
"2026-01-03T00:00:00Z",
|
|
128
|
+
"2026-01-02T00:00:00Z",
|
|
129
|
+
"2026-01-01T00:00:00Z",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_should_classify_dirty_review_when_body_matches_bugbot_findings_pattern() -> (
|
|
134
|
+
None
|
|
135
|
+
):
|
|
136
|
+
pages_payload = json.dumps(
|
|
137
|
+
[
|
|
138
|
+
[
|
|
139
|
+
{
|
|
140
|
+
"id": 1,
|
|
141
|
+
"user": {"login": "cursor[bot]"},
|
|
142
|
+
"commit_id": "abc",
|
|
143
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
144
|
+
"body": "Cursor Bugbot has reviewed your changes and found 3 potential issues.",
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
with patch("subprocess.run") as mock_run:
|
|
150
|
+
mock_run.return_value = _completed(pages_payload)
|
|
151
|
+
all_reviews = fetch_bugbot_reviews_module.fetch_bugbot_reviews(
|
|
152
|
+
owner="acme", repo="widget", number=42
|
|
153
|
+
)
|
|
154
|
+
assert all_reviews[0]["classification"] == "dirty"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_should_classify_clean_review_when_body_lacks_findings_pattern() -> None:
|
|
158
|
+
pages_payload = json.dumps(
|
|
159
|
+
[
|
|
160
|
+
[
|
|
161
|
+
{
|
|
162
|
+
"id": 1,
|
|
163
|
+
"user": {"login": "cursor[bot]"},
|
|
164
|
+
"commit_id": "abc",
|
|
165
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
166
|
+
"body": "Bugbot reviewed your changes and found no new issues!",
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
with patch("subprocess.run") as mock_run:
|
|
172
|
+
mock_run.return_value = _completed(pages_payload)
|
|
173
|
+
all_reviews = fetch_bugbot_reviews_module.fetch_bugbot_reviews(
|
|
174
|
+
owner="acme", repo="widget", number=42
|
|
175
|
+
)
|
|
176
|
+
assert all_reviews[0]["classification"] == "clean"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_should_reference_cursor_bot_login_constant_directly_without_local_alias() -> None:
|
|
180
|
+
source_text = (
|
|
181
|
+
Path(__file__).resolve().parent / "fetch_bugbot_reviews.py"
|
|
182
|
+
).read_text(encoding="utf-8")
|
|
183
|
+
assert "cursor_bot_login = CURSOR_BOT_LOGIN" not in source_text
|
|
184
|
+
assert "CURSOR_BOT_LOGIN" in source_text
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_should_raise_when_gh_subprocess_fails() -> None:
|
|
188
|
+
failure = subprocess.CalledProcessError(
|
|
189
|
+
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
190
|
+
)
|
|
191
|
+
with patch("subprocess.run", side_effect=failure):
|
|
192
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
193
|
+
fetch_bugbot_reviews_module.fetch_bugbot_reviews(
|
|
194
|
+
owner="acme", repo="widget", number=42
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_should_return_entries_whose_keys_are_strings() -> None:
|
|
199
|
+
pages_payload = json.dumps(
|
|
200
|
+
[
|
|
201
|
+
[
|
|
202
|
+
{
|
|
203
|
+
"id": 1,
|
|
204
|
+
"user": {"login": "cursor[bot]"},
|
|
205
|
+
"commit_id": "abc",
|
|
206
|
+
"submitted_at": "2026-01-01T00:00:00Z",
|
|
207
|
+
"body": "Bugbot reviewed your changes and found no new issues!",
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
]
|
|
211
|
+
)
|
|
212
|
+
with patch("subprocess.run") as mock_run:
|
|
213
|
+
mock_run.return_value = _completed(pages_payload)
|
|
214
|
+
all_reviews = fetch_bugbot_reviews_module.fetch_bugbot_reviews(
|
|
215
|
+
owner="acme", repo="widget", number=42
|
|
216
|
+
)
|
|
217
|
+
assert len(all_reviews) == 1
|
|
218
|
+
first_review_entry = all_reviews[0]
|
|
219
|
+
assert isinstance(first_review_entry, dict)
|
|
220
|
+
assert all(isinstance(each_key, str) for each_key in first_review_entry.keys())
|
|
@@ -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())
|