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.
Files changed (76) hide show
  1. package/_shared/pr-loop/scripts/config/preflight_constants.py +29 -8
  2. package/_shared/pr-loop/scripts/preflight.py +242 -20
  3. package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
  4. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
  5. package/hooks/blocking/code_rules_enforcer.py +269 -23
  6. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
  7. package/hooks/config/test_unused_module_import_constants.py +48 -0
  8. package/hooks/config/unused_module_import_constants.py +41 -0
  9. package/package.json +1 -1
  10. package/rules/gh-paginate.md +4 -50
  11. package/rules/no-historical-clutter.md +36 -0
  12. package/skills/bg-agent/SKILL.md +69 -0
  13. package/skills/bugteam/CONSTRAINTS.md +10 -19
  14. package/skills/bugteam/PROMPTS.md +21 -14
  15. package/skills/bugteam/SKILL.md +122 -208
  16. package/skills/bugteam/SKILL_EVALS.md +75 -114
  17. package/skills/bugteam/reference/README.md +2 -4
  18. package/skills/bugteam/reference/audit-and-teammates.md +21 -48
  19. package/skills/bugteam/reference/audit-contract.md +7 -7
  20. package/skills/bugteam/reference/design-rationale.md +3 -8
  21. package/skills/bugteam/reference/team-setup.md +11 -19
  22. package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
  23. package/skills/bugteam/scripts/config/__init__.py +0 -0
  24. package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
  25. package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
  26. package/skills/bugteam/sources.md +1 -25
  27. package/skills/bugteam/test_skill_additions.py +4 -13
  28. package/skills/fresh-branch/SKILL.md +71 -0
  29. package/skills/gotcha/SKILL.md +73 -0
  30. package/skills/monitor-open-prs/SKILL.md +4 -37
  31. package/skills/monitor-open-prs/test_skill_contract.py +0 -5
  32. package/skills/pr-converge/SKILL.md +60 -1298
  33. package/skills/pr-converge/reference/convergence-gates.md +122 -0
  34. package/skills/pr-converge/reference/examples.md +76 -0
  35. package/skills/pr-converge/reference/fix-protocol.md +56 -0
  36. package/skills/pr-converge/reference/ground-rules.md +13 -0
  37. package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
  38. package/skills/pr-converge/reference/per-tick.md +204 -0
  39. package/skills/pr-converge/reference/state-schema.md +19 -0
  40. package/skills/pr-converge/reference/stop-conditions.md +26 -0
  41. package/skills/pr-converge/scripts/README.md +36 -9
  42. package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
  43. package/skills/pr-converge/scripts/config/pr_converge_constants.py +74 -5
  44. package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
  45. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
  46. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
  47. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
  48. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
  49. package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
  50. package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
  51. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
  52. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
  53. package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
  54. package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
  55. package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
  56. package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
  57. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
  58. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
  59. package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
  60. package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
  61. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
  62. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
  63. package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
  64. package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
  65. package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
  66. package/skills/pr-converge/scripts/test_view_pr_context.py +44 -0
  67. package/skills/pr-converge/scripts/view_pr_context.py +35 -4
  68. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
  69. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
  70. package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
  71. package/skills/bugteam/test_team_lifecycle.py +0 -103
  72. package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
  73. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
  74. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
  75. package/skills/pr-converge/test_team_lifecycle.py +0 -56
  76. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +0 -108
@@ -0,0 +1,162 @@
1
+ """Tests for reflow_skill_md.
2
+
3
+ Covers:
4
+ - wrap_long_bash_line returns unchanged when indent leaves zero/negative width
5
+ - wrap_long_bash_fence_lines handles deeply-indented bash content safely
6
+ - structural string literals are imported from config, not inline
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ import sys
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+
16
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
17
+
18
+
19
+ def _load_module() -> ModuleType:
20
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
21
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
22
+ module_path = _SCRIPTS_DIRECTORY / "reflow_skill_md.py"
23
+ spec = importlib.util.spec_from_file_location("reflow_skill_md", module_path)
24
+ assert spec is not None
25
+ assert spec.loader is not None
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ reflow_module = _load_module()
32
+
33
+
34
+ def test_should_preserve_pr_converge_state_json_path_in_yaml_description() -> None:
35
+ lines = [
36
+ " Multi-PR runs persist traffic in",
37
+ " `<TMPDIR>/pr-converge-<session_id>/state.json` per Multi-PR",
38
+ " orchestration model.",
39
+ "---",
40
+ ]
41
+ all_reflowed_lines, next_index = reflow_module.reflow_yaml_description_block(lines, 0)
42
+ reflowed_description = " ".join(each_line.strip() for each_line in all_reflowed_lines)
43
+
44
+ assert next_index == len(lines)
45
+ assert "`<TMPDIR>/pr-converge-<session_id>/state.json`" in reflowed_description
46
+ assert "`<TMPDIR>/pr-converge-<session_id>/state.json>`" not in reflowed_description
47
+
48
+
49
+ def test_wrap_long_bash_fence_lines_does_not_modify_deeply_indented_line() -> None:
50
+ """When indentation >= MAX_WIDTH, return line as-is."""
51
+ deep_indent = " " * 85
52
+ long_line = deep_indent + "echo hello world this is a long command"
53
+ all_input_lines = ["```bash", long_line, "```"]
54
+ all_result_lines = reflow_module.wrap_long_bash_fence_lines(all_input_lines)
55
+ assert all_result_lines == ["```bash", long_line, "```"]
56
+
57
+
58
+ def test_wrap_long_bash_fence_lines_does_not_modify_max_width_indent_line() -> None:
59
+ """When indentation == MAX_WIDTH, return line as-is."""
60
+ deep_indent = " " * 80
61
+ long_line = deep_indent + "echo hello"
62
+ all_input_lines = ["```bash", long_line, "```"]
63
+ all_result_lines = reflow_module.wrap_long_bash_fence_lines(all_input_lines)
64
+ assert all_result_lines == ["```bash", long_line, "```"]
65
+
66
+
67
+ def test_wrap_long_bash_fence_lines_handles_deeply_indented_bash() -> None:
68
+ """Full pipeline does not hang on bash fence with extreme indentation."""
69
+ deep_indent = " " * 90
70
+ all_input_lines = [
71
+ "```bash",
72
+ deep_indent + "some_command --flag value --other long argument text here",
73
+ "```",
74
+ ]
75
+ all_result_lines = reflow_module.wrap_long_bash_fence_lines(all_input_lines)
76
+ assert len(all_result_lines) == 3
77
+ assert all_result_lines[0] == "```bash"
78
+ assert all_result_lines[2] == "```"
79
+ assert all_result_lines[1] == all_input_lines[1]
80
+
81
+
82
+ def test_is_new_logical_line_recognizes_fence_via_constant() -> None:
83
+ """Code fence detection uses the imported constant marker."""
84
+ assert reflow_module.is_new_logical_line("```bash") is True
85
+ assert reflow_module.is_new_logical_line("```") is True
86
+
87
+
88
+ def test_reflow_merged_line_recognizes_example_tags_via_constant() -> None:
89
+ """Example tag detection uses imported constant markers."""
90
+ assert reflow_module.reflow_merged_line("<example>") == ["<example>"]
91
+ close_result = reflow_module.reflow_merged_line("</example>")
92
+ assert close_result == ["</example>"]
93
+
94
+
95
+ def test_reflow_merged_line_recognizes_yaml_delimiter_via_constant() -> None:
96
+ """YAML delimiter detection uses imported constant."""
97
+ assert reflow_module.reflow_merged_line("---") == ["---"]
98
+
99
+
100
+ def test_reflow_merged_line_preserves_long_markdown_reference_definition() -> None:
101
+ """Lines matching reference definitions survive reflow without paragraph wrapping."""
102
+ long_url_token = "x" * 90
103
+ line = f"[bugbot-ref]: https://example.com/{long_url_token}"
104
+ stripped_line = line.strip()
105
+ maximum_width = reflow_module.MAX_WIDTH
106
+ assert len(stripped_line) > maximum_width
107
+ assert reflow_module.REF_DEF_RE.match(stripped_line) is not None
108
+ assert reflow_module.reflow_merged_line(line) == [stripped_line]
109
+
110
+
111
+ def test_reflow_bootstrap_moves_script_directory_ahead_of_shadow_config(
112
+ tmp_path: Path,
113
+ ) -> None:
114
+ """sys.path bootstrap must move the script directory ahead of shadow config packages."""
115
+ shadow_config_directory = tmp_path / "shadow" / "config"
116
+ shadow_config_directory.mkdir(parents=True)
117
+ (shadow_config_directory / "__init__.py").write_text("", encoding="utf-8")
118
+ (shadow_config_directory / "pr_converge_constants.py").write_text(
119
+ "BROKEN = True\n", encoding="utf-8"
120
+ )
121
+ original_sys_path = list(sys.path)
122
+ try:
123
+ sys.path.insert(0, str(tmp_path / "shadow"))
124
+ loaded_module = _load_module()
125
+ assert loaded_module.MAX_WIDTH == 80
126
+ assert sys.path[0] == str(_SCRIPTS_DIRECTORY)
127
+ assert sys.path.count(str(_SCRIPTS_DIRECTORY)) == 1
128
+ finally:
129
+ sys.path[:] = original_sys_path
130
+
131
+
132
+ def test_wrap_long_bash_fence_lines_uses_continuation_marker_for_long_lines() -> None:
133
+ """Wrapped continuation lines use the bash continuation marker."""
134
+ long_line = "echo " + "word " * 20
135
+ all_input_lines = ["```bash", long_line, "```"]
136
+ all_result_lines = reflow_module.wrap_long_bash_fence_lines(all_input_lines)
137
+ wrapped_content = all_result_lines[1:-1]
138
+ assert len(wrapped_content) > 1, "Line must be long enough to wrap"
139
+ assert wrapped_content[-1].lstrip(), "Final segment must have content"
140
+ assert all(len(each) <= reflow_module.MAX_WIDTH for each in wrapped_content), (
141
+ "All wrapped lines must be within MAX_WIDTH"
142
+ )
143
+
144
+
145
+ def test_reflow_uses_config_constant_for_continuation_marker_width() -> None:
146
+ """The bash continuation marker width must come from config, not inline."""
147
+ module_path = _SCRIPTS_DIRECTORY / "reflow_skill_md.py"
148
+ source = module_path.read_text(encoding="utf-8")
149
+ assert "BASH_CONTINUATION_MARKER_WIDTH" in source, (
150
+ "reflow_skill_md.py must import BASH_CONTINUATION_MARKER_WIDTH from config"
151
+ )
152
+
153
+ def test_reflow_bootstrap_matches_code_rules_sys_path_pattern() -> None:
154
+ """Bootstrap must guard insert with a membership check."""
155
+ module_path = _SCRIPTS_DIRECTORY / "reflow_skill_md.py"
156
+ source = module_path.read_text(encoding="utf-8")
157
+ assert "while script_directory in sys.path:" in source, (
158
+ "Bootstrap must dedup script_directory entries before insert"
159
+ )
160
+ assert "sys.path.insert(0, script_directory)" in source, (
161
+ "Bootstrap must insert script_directory at index 0"
162
+ )
@@ -0,0 +1,448 @@
1
+ """Tests for reviewer_fetch_core.
2
+
3
+ Covers:
4
+ - fetch_reviewer_reviews invokes gh against the reviews endpoint with --paginate --slurp
5
+ - login filter applies case-insensitively as a substring on user.login
6
+ - entries missing submitted_at or id are filtered out
7
+ - reviews are sorted newest-first by submitted_at
8
+ - the spec.classify_review callable is invoked for each surviving review
9
+ - subprocess errors propagate
10
+ - fetch_reviewer_inline_comments returns empty when no review for current_head
11
+ - fetch_reviewer_inline_comments only returns comments anchored to the latest review
12
+ - fetch_reviewer_inline_comments invokes gh against the comments endpoint with --paginate --slurp
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import importlib.util
18
+ import json
19
+ import subprocess
20
+ from pathlib import Path
21
+ from types import ModuleType
22
+ from unittest.mock import MagicMock, patch
23
+
24
+
25
+ def _load_module() -> ModuleType:
26
+ module_path = Path(__file__).parent / "reviewer_fetch_core.py"
27
+ spec = importlib.util.spec_from_file_location("reviewer_fetch_core", module_path)
28
+ assert spec is not None
29
+ assert spec.loader is not None
30
+ module = importlib.util.module_from_spec(spec)
31
+ spec.loader.exec_module(module)
32
+ return module
33
+
34
+
35
+ reviewer_fetch_core_module = _load_module()
36
+
37
+
38
+ def _completed(stdout: str) -> subprocess.CompletedProcess:
39
+ process = MagicMock(spec=subprocess.CompletedProcess)
40
+ process.stdout = stdout
41
+ process.returncode = 0
42
+ return process
43
+
44
+
45
+ def _fake_spec(*, login_filter_substring: str = "test") -> object:
46
+ fake_spec_object = MagicMock()
47
+ fake_spec_object.login_filter_substring = login_filter_substring
48
+ fake_spec_object.classify_review = MagicMock(return_value="clean")
49
+ return fake_spec_object
50
+
51
+
52
+ def test_fetch_reviewer_reviews_invokes_gh_with_paginate_slurp_against_reviews_endpoint() -> (
53
+ None
54
+ ):
55
+ pages_payload = json.dumps([[]])
56
+ with patch("subprocess.run") as mock_run:
57
+ mock_run.return_value = _completed(pages_payload)
58
+ reviewer_fetch_core_module.fetch_reviewer_reviews(
59
+ _fake_spec(), owner="acme", repo="widget", number=42
60
+ )
61
+ invoked_argv = mock_run.call_args[0][0]
62
+ assert invoked_argv[0] == "gh"
63
+ assert invoked_argv[1] == "api"
64
+ assert "repos/acme/widget/pulls/42/reviews?per_page=100" in invoked_argv[2]
65
+ assert "--paginate" in invoked_argv
66
+ assert "--slurp" in invoked_argv
67
+
68
+
69
+ def test_fetch_reviewer_reviews_filters_by_login_filter_substring_case_insensitively() -> (
70
+ None
71
+ ):
72
+ pages_payload = json.dumps(
73
+ [
74
+ [
75
+ {
76
+ "id": 1,
77
+ "user": {"login": "TestBot[bot]"},
78
+ "state": "APPROVED",
79
+ "commit_id": "abc",
80
+ "submitted_at": "2026-01-01T00:00:00Z",
81
+ "body": "uppercase login",
82
+ },
83
+ {
84
+ "id": 2,
85
+ "user": {"login": "other-reviewer"},
86
+ "state": "APPROVED",
87
+ "commit_id": "abc",
88
+ "submitted_at": "2026-01-02T00:00:00Z",
89
+ "body": "no match",
90
+ },
91
+ ]
92
+ ]
93
+ )
94
+ with patch("subprocess.run") as mock_run:
95
+ mock_run.return_value = _completed(pages_payload)
96
+ all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
97
+ _fake_spec(login_filter_substring="test"),
98
+ owner="acme",
99
+ repo="widget",
100
+ number=42,
101
+ )
102
+ assert len(all_reviews) == 1
103
+ assert all_reviews[0]["review_id"] == 1
104
+
105
+
106
+ def test_fetch_reviewer_reviews_drops_entries_missing_submitted_at() -> None:
107
+ pages_payload = json.dumps(
108
+ [
109
+ [
110
+ {
111
+ "id": 1,
112
+ "user": {"login": "test[bot]"},
113
+ "state": "APPROVED",
114
+ "commit_id": "abc",
115
+ "body": "no submitted_at",
116
+ },
117
+ {
118
+ "id": 2,
119
+ "user": {"login": "test[bot]"},
120
+ "state": "APPROVED",
121
+ "commit_id": "abc",
122
+ "submitted_at": "2026-01-02T00:00:00Z",
123
+ "body": "valid",
124
+ },
125
+ ]
126
+ ]
127
+ )
128
+ with patch("subprocess.run") as mock_run:
129
+ mock_run.return_value = _completed(pages_payload)
130
+ all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
131
+ _fake_spec(), owner="acme", repo="widget", number=42
132
+ )
133
+ assert [each_review["review_id"] for each_review in all_reviews] == [2]
134
+
135
+
136
+ def test_fetch_reviewer_reviews_drops_entries_missing_id() -> None:
137
+ pages_payload = json.dumps(
138
+ [
139
+ [
140
+ {
141
+ "user": {"login": "test[bot]"},
142
+ "state": "APPROVED",
143
+ "commit_id": "abc",
144
+ "submitted_at": "2026-01-01T00:00:00Z",
145
+ "body": "no id",
146
+ },
147
+ {
148
+ "id": 99,
149
+ "user": {"login": "test[bot]"},
150
+ "state": "APPROVED",
151
+ "commit_id": "abc",
152
+ "submitted_at": "2026-01-02T00:00:00Z",
153
+ "body": "valid",
154
+ },
155
+ ]
156
+ ]
157
+ )
158
+ with patch("subprocess.run") as mock_run:
159
+ mock_run.return_value = _completed(pages_payload)
160
+ all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
161
+ _fake_spec(), owner="acme", repo="widget", number=42
162
+ )
163
+ assert [each_review["review_id"] for each_review in all_reviews] == [99]
164
+
165
+
166
+ def test_fetch_reviewer_reviews_sorts_newest_first_across_pages() -> None:
167
+ pages_payload = json.dumps(
168
+ [
169
+ [
170
+ {
171
+ "id": 10,
172
+ "user": {"login": "test[bot]"},
173
+ "state": "APPROVED",
174
+ "commit_id": "old",
175
+ "submitted_at": "2026-01-01T00:00:00Z",
176
+ "body": "oldest",
177
+ }
178
+ ],
179
+ [
180
+ {
181
+ "id": 11,
182
+ "user": {"login": "test[bot]"},
183
+ "state": "CHANGES_REQUESTED",
184
+ "commit_id": "new",
185
+ "submitted_at": "2026-01-03T00:00:00Z",
186
+ "body": "newest",
187
+ },
188
+ {
189
+ "id": 12,
190
+ "user": {"login": "test[bot]"},
191
+ "state": "APPROVED",
192
+ "commit_id": "mid",
193
+ "submitted_at": "2026-01-02T00:00:00Z",
194
+ "body": "middle",
195
+ },
196
+ ],
197
+ ]
198
+ )
199
+ with patch("subprocess.run") as mock_run:
200
+ mock_run.return_value = _completed(pages_payload)
201
+ all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
202
+ _fake_spec(), owner="acme", repo="widget", number=42
203
+ )
204
+ assert [each_review["submitted_at"] for each_review in all_reviews] == [
205
+ "2026-01-03T00:00:00Z",
206
+ "2026-01-02T00:00:00Z",
207
+ "2026-01-01T00:00:00Z",
208
+ ]
209
+
210
+
211
+ def test_fetch_reviewer_reviews_invokes_classify_callable_per_review() -> None:
212
+ pages_payload = json.dumps(
213
+ [
214
+ [
215
+ {
216
+ "id": 1,
217
+ "user": {"login": "test[bot]"},
218
+ "state": "APPROVED",
219
+ "commit_id": "abc",
220
+ "submitted_at": "2026-01-01T00:00:00Z",
221
+ "body": "first",
222
+ },
223
+ {
224
+ "id": 2,
225
+ "user": {"login": "test[bot]"},
226
+ "state": "CHANGES_REQUESTED",
227
+ "commit_id": "abc",
228
+ "submitted_at": "2026-01-02T00:00:00Z",
229
+ "body": "second",
230
+ },
231
+ ]
232
+ ]
233
+ )
234
+ classify_callable = MagicMock(side_effect=["dirty", "clean"])
235
+ fake_spec_object = MagicMock()
236
+ fake_spec_object.login_filter_substring = "test"
237
+ fake_spec_object.classify_review = classify_callable
238
+ with patch("subprocess.run") as mock_run:
239
+ mock_run.return_value = _completed(pages_payload)
240
+ all_reviews = reviewer_fetch_core_module.fetch_reviewer_reviews(
241
+ fake_spec_object, owner="acme", repo="widget", number=42
242
+ )
243
+ assert classify_callable.call_count == 2
244
+ assert {each_review["classification"] for each_review in all_reviews} == {
245
+ "dirty",
246
+ "clean",
247
+ }
248
+
249
+
250
+ def test_fetch_reviewer_reviews_propagates_subprocess_errors() -> None:
251
+ failure = subprocess.CalledProcessError(
252
+ returncode=1, cmd=["gh"], stderr="auth failure"
253
+ )
254
+ with patch("subprocess.run", side_effect=failure):
255
+ try:
256
+ reviewer_fetch_core_module.fetch_reviewer_reviews(
257
+ _fake_spec(), owner="acme", repo="widget", number=42
258
+ )
259
+ assert False, "expected CalledProcessError"
260
+ except subprocess.CalledProcessError:
261
+ pass
262
+
263
+
264
+ def test_fetch_reviewer_inline_comments_returns_empty_when_no_review_for_head() -> None:
265
+ no_matching_review = [
266
+ {
267
+ "review_id": 1,
268
+ "commit_id": "other_sha",
269
+ "submitted_at": "2026-01-01T00:00:00Z",
270
+ "state": "APPROVED",
271
+ "body": "",
272
+ "classification": "clean",
273
+ }
274
+ ]
275
+ with patch("subprocess.run") as mock_run:
276
+ all_inline_comments = reviewer_fetch_core_module.fetch_reviewer_inline_comments(
277
+ _fake_spec(),
278
+ owner="acme",
279
+ repo="widget",
280
+ number=42,
281
+ current_head="missing_sha",
282
+ all_reviews=no_matching_review,
283
+ )
284
+ assert all_inline_comments == []
285
+ mock_run.assert_not_called()
286
+
287
+
288
+ def test_fetch_reviewer_inline_comments_invokes_gh_against_comments_endpoint() -> None:
289
+ pages_payload = json.dumps([[]])
290
+ matching_review = [
291
+ {
292
+ "review_id": 1,
293
+ "commit_id": "abc123",
294
+ "submitted_at": "2026-01-01T00:00:00Z",
295
+ "state": "APPROVED",
296
+ "body": "",
297
+ "classification": "clean",
298
+ }
299
+ ]
300
+ with patch("subprocess.run") as mock_run:
301
+ mock_run.return_value = _completed(pages_payload)
302
+ reviewer_fetch_core_module.fetch_reviewer_inline_comments(
303
+ _fake_spec(),
304
+ owner="acme",
305
+ repo="widget",
306
+ number=42,
307
+ current_head="abc123",
308
+ all_reviews=matching_review,
309
+ )
310
+ invoked_argv = mock_run.call_args[0][0]
311
+ assert invoked_argv[0] == "gh"
312
+ assert invoked_argv[1] == "api"
313
+ assert "repos/acme/widget/pulls/42/comments?per_page=100" in invoked_argv[2]
314
+ assert "--paginate" in invoked_argv
315
+ assert "--slurp" in invoked_argv
316
+
317
+
318
+ def test_fetch_reviewer_inline_comments_anchors_to_latest_review_id_for_head() -> None:
319
+ reviews_newest_first = [
320
+ {
321
+ "review_id": 11,
322
+ "commit_id": "same_sha",
323
+ "submitted_at": "2026-01-02T00:00:00Z",
324
+ "state": "APPROVED",
325
+ "body": "lgtm",
326
+ "classification": "clean",
327
+ },
328
+ {
329
+ "review_id": 10,
330
+ "commit_id": "same_sha",
331
+ "submitted_at": "2026-01-01T00:00:00Z",
332
+ "state": "CHANGES_REQUESTED",
333
+ "body": "fix",
334
+ "classification": "dirty",
335
+ },
336
+ ]
337
+ pages_payload = json.dumps(
338
+ [
339
+ [
340
+ {
341
+ "id": 100,
342
+ "user": {"login": "test[bot]"},
343
+ "commit_id": "same_sha",
344
+ "pull_request_review_id": 10,
345
+ "body": "stale",
346
+ "path": "x.py",
347
+ "line": 1,
348
+ },
349
+ {
350
+ "id": 101,
351
+ "user": {"login": "test[bot]"},
352
+ "commit_id": "same_sha",
353
+ "pull_request_review_id": 11,
354
+ "body": "current",
355
+ "path": "x.py",
356
+ "line": 2,
357
+ },
358
+ ]
359
+ ]
360
+ )
361
+ with patch("subprocess.run") as mock_run:
362
+ mock_run.return_value = _completed(pages_payload)
363
+ all_inline_comments = reviewer_fetch_core_module.fetch_reviewer_inline_comments(
364
+ _fake_spec(),
365
+ owner="acme",
366
+ repo="widget",
367
+ number=42,
368
+ current_head="same_sha",
369
+ all_reviews=reviews_newest_first,
370
+ )
371
+ assert [each_comment["comment_id"] for each_comment in all_inline_comments] == [101]
372
+
373
+
374
+ def test_fetch_reviewer_inline_comments_filters_login_substring() -> None:
375
+ matching_review = [
376
+ {
377
+ "review_id": 9,
378
+ "commit_id": "abc",
379
+ "submitted_at": "2026-01-01T00:00:00Z",
380
+ "state": "APPROVED",
381
+ "body": "",
382
+ "classification": "clean",
383
+ }
384
+ ]
385
+ pages_payload = json.dumps(
386
+ [
387
+ [
388
+ {
389
+ "id": 1,
390
+ "user": {"login": "test[bot]"},
391
+ "commit_id": "abc",
392
+ "pull_request_review_id": 9,
393
+ "body": "match",
394
+ "path": "f.py",
395
+ "line": 1,
396
+ },
397
+ {
398
+ "id": 2,
399
+ "user": {"login": "other-reviewer"},
400
+ "commit_id": "abc",
401
+ "pull_request_review_id": 9,
402
+ "body": "no match",
403
+ "path": "f.py",
404
+ "line": 2,
405
+ },
406
+ ]
407
+ ]
408
+ )
409
+ with patch("subprocess.run") as mock_run:
410
+ mock_run.return_value = _completed(pages_payload)
411
+ all_inline_comments = reviewer_fetch_core_module.fetch_reviewer_inline_comments(
412
+ _fake_spec(login_filter_substring="test"),
413
+ owner="acme",
414
+ repo="widget",
415
+ number=42,
416
+ current_head="abc",
417
+ all_reviews=matching_review,
418
+ )
419
+ assert [each_comment["comment_id"] for each_comment in all_inline_comments] == [1]
420
+
421
+
422
+ def test_fetch_reviewer_inline_comments_propagates_subprocess_errors() -> None:
423
+ matching_review = [
424
+ {
425
+ "review_id": 1,
426
+ "commit_id": "abc",
427
+ "submitted_at": "2026-01-01T00:00:00Z",
428
+ "state": "APPROVED",
429
+ "body": "",
430
+ "classification": "clean",
431
+ }
432
+ ]
433
+ failure = subprocess.CalledProcessError(
434
+ returncode=1, cmd=["gh"], stderr="auth failure"
435
+ )
436
+ with patch("subprocess.run", side_effect=failure):
437
+ try:
438
+ reviewer_fetch_core_module.fetch_reviewer_inline_comments(
439
+ _fake_spec(),
440
+ owner="acme",
441
+ repo="widget",
442
+ number=42,
443
+ current_head="abc",
444
+ all_reviews=matching_review,
445
+ )
446
+ assert False, "expected CalledProcessError"
447
+ except subprocess.CalledProcessError:
448
+ pass