claude-dev-env 1.37.0 → 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.
Files changed (95) hide show
  1. package/CLAUDE.md +3 -0
  2. package/_shared/pr-loop/audit-contract.md +4 -3
  3. package/_shared/pr-loop/fix-protocol.md +2 -0
  4. package/_shared/pr-loop/gh-payloads.md +38 -37
  5. package/_shared/pr-loop/scripts/README.md +0 -1
  6. package/_shared/pr-loop/scripts/preflight.py +2 -1
  7. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +2 -2
  8. package/_shared/pr-loop/scripts/tests/test_preflight.py +22 -0
  9. package/_shared/pr-loop/state-schema.md +10 -10
  10. package/agents/clean-coder.md +4 -0
  11. package/agents/code-quality-agent.md +23 -85
  12. package/agents/groq-coder.md +8 -6
  13. package/hooks/blocking/__init__.py +0 -0
  14. package/hooks/blocking/hedging_language_blocker.py +2 -2
  15. package/hooks/blocking/state_description_blocker.py +243 -0
  16. package/hooks/blocking/tdd_enforcer.py +94 -0
  17. package/hooks/blocking/test_hedging_language_blocker.py +1 -1
  18. package/hooks/blocking/test_state_description_blocker.py +618 -0
  19. package/hooks/blocking/test_tdd_enforcer.py +152 -0
  20. package/hooks/config/state_description_blocker_constants.py +130 -0
  21. package/hooks/hooks.json +10 -0
  22. package/package.json +1 -1
  23. package/rules/gh-paginate.md +4 -50
  24. package/rules/no-historical-clutter.md +57 -0
  25. package/scripts/config/groq_bugteam_config.py +13 -5
  26. package/skills/bugteam/CONSTRAINTS.md +20 -27
  27. package/skills/bugteam/EXAMPLES.md +1 -1
  28. package/skills/bugteam/PROMPTS.md +78 -42
  29. package/skills/bugteam/SKILL.md +76 -63
  30. package/skills/bugteam/SKILL_EVALS.md +12 -12
  31. package/skills/bugteam/reference/audit-and-teammates.md +21 -48
  32. package/skills/bugteam/reference/audit-contract.md +7 -7
  33. package/skills/bugteam/reference/github-pr-reviews.md +31 -31
  34. package/skills/bugteam/reference/team-setup.md +1 -1
  35. package/skills/bugteam/reference/teardown-publish-permissions.md +4 -4
  36. package/skills/copilot-review/SKILL.md +7 -14
  37. package/skills/findbugs/SKILL.md +2 -2
  38. package/skills/fixbugs/SKILL.md +1 -1
  39. package/skills/monitor-open-prs/SKILL.md +6 -6
  40. package/skills/pr-converge/SKILL.md +7 -6
  41. package/skills/pr-converge/reference/convergence-gates.md +46 -44
  42. package/skills/pr-converge/reference/examples.md +4 -4
  43. package/skills/pr-converge/reference/fix-protocol.md +8 -8
  44. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -10
  45. package/skills/pr-converge/reference/per-tick.md +24 -36
  46. package/skills/pr-converge/reference/stop-conditions.md +7 -7
  47. package/skills/pr-converge/scripts/README.md +65 -117
  48. package/skills/pr-review-responder/EXAMPLES.md +2 -2
  49. package/skills/pr-review-responder/PRINCIPLES.md +2 -8
  50. package/skills/pr-review-responder/README.md +7 -48
  51. package/skills/pr-review-responder/SKILL.md +2 -3
  52. package/skills/pr-review-responder/TESTING.md +8 -65
  53. package/skills/qbug/SKILL.md +10 -16
  54. package/_shared/pr-loop/scripts/config/gh_util_constants.py +0 -31
  55. package/_shared/pr-loop/scripts/gh_util.py +0 -193
  56. package/_shared/pr-loop/scripts/tests/test_gh_util.py +0 -257
  57. package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +0 -61
  58. package/skills/pr-converge/scripts/check_pr_mergeability.py +0 -78
  59. package/skills/pr-converge/scripts/config/pr_converge_constants.py +0 -118
  60. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -152
  61. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +0 -70
  62. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +0 -57
  63. package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +0 -70
  64. package/skills/pr-converge/scripts/fetch_claude_reviews.py +0 -61
  65. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +0 -70
  66. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +0 -61
  67. package/skills/pr-converge/scripts/mark_pr_ready.py +0 -54
  68. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +0 -49
  69. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +0 -33
  70. package/skills/pr-converge/scripts/reply_to_inline_comment.py +0 -84
  71. package/skills/pr-converge/scripts/request_copilot_review.py +0 -71
  72. package/skills/pr-converge/scripts/resolve_pr_head.py +0 -58
  73. package/skills/pr-converge/scripts/review_field_helpers.py +0 -43
  74. package/skills/pr-converge/scripts/reviewer_fetch_core.py +0 -153
  75. package/skills/pr-converge/scripts/reviewer_specs.py +0 -98
  76. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +0 -126
  77. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +0 -443
  78. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +0 -299
  79. package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +0 -485
  80. package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +0 -368
  81. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +0 -440
  82. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +0 -366
  83. package/skills/pr-converge/scripts/test_mark_pr_ready.py +0 -69
  84. package/skills/pr-converge/scripts/test_post_bugbot_run.py +0 -195
  85. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +0 -159
  86. package/skills/pr-converge/scripts/test_request_copilot_review.py +0 -101
  87. package/skills/pr-converge/scripts/test_resolve_pr_head.py +0 -79
  88. package/skills/pr-converge/scripts/test_review_field_helpers.py +0 -80
  89. package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +0 -448
  90. package/skills/pr-converge/scripts/test_reviewer_specs.py +0 -107
  91. package/skills/pr-converge/scripts/test_trigger_bugbot.py +0 -139
  92. package/skills/pr-converge/scripts/test_view_pr_context.py +0 -111
  93. package/skills/pr-converge/scripts/trigger_bugbot.py +0 -77
  94. package/skills/pr-converge/scripts/view_pr_context.py +0 -47
  95. package/skills/pr-review-responder/scripts/respond_to_reviews.py +0 -376
@@ -1,193 +0,0 @@
1
- """Shared helpers for invoking GitHub CLI with basic resiliency."""
2
-
3
- import json
4
- import subprocess
5
- import sys
6
- import time
7
- from dataclasses import dataclass
8
- from pathlib import Path
9
- from typing import Sequence
10
-
11
- sys.modules.pop("config", None)
12
- if str(Path(__file__).resolve().parent) not in sys.path:
13
- sys.path.insert(0, str(Path(__file__).resolve().parent))
14
-
15
- from config.gh_util_constants import (
16
- ALL_AUTH_ERROR_MARKERS,
17
- ALL_TRANSIENT_ERROR_MARKERS,
18
- DEFAULT_BACKOFF_SECONDS,
19
- DEFAULT_RETRIES,
20
- DEFAULT_TIMEOUT_SECONDS,
21
- EXPONENTIAL_BACKOFF_BASE,
22
- GH_TIMEOUT_RETURN_CODE,
23
- INLINE_REVIEW_COMMENTS_PATH_TEMPLATE,
24
- )
25
-
26
-
27
- @dataclass(frozen=True)
28
- class GhResult:
29
- returncode: int
30
- stdout: str
31
- stderr: str
32
- is_timed_out: bool = False
33
-
34
-
35
- def _is_transient_error(message: str) -> bool:
36
- lowered = message.lower()
37
- return any(each_marker in lowered for each_marker in ALL_TRANSIENT_ERROR_MARKERS)
38
-
39
-
40
- def _is_auth_error(message: str) -> bool:
41
- lowered = message.lower()
42
- return any(each_marker in lowered for each_marker in ALL_AUTH_ERROR_MARKERS)
43
-
44
-
45
- def _ensure_text(text_or_bytes: str | bytes | None) -> str:
46
- if text_or_bytes is None:
47
- return ""
48
- if isinstance(text_or_bytes, bytes):
49
- return text_or_bytes.decode(errors="replace")
50
- return text_or_bytes
51
-
52
-
53
- def run_gh(
54
- all_command: Sequence[str],
55
- *,
56
- timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
57
- ) -> GhResult:
58
- """Run a gh command with timeout + transient retry handling.
59
-
60
- Retries are attempted only for transient failures (network/server/rate-limit style
61
- messages). Auth/scope failures are returned immediately to fail closed.
62
- """
63
- if timeout_seconds <= 0:
64
- raise ValueError("timeout_seconds must be positive")
65
- max_attempts = DEFAULT_RETRIES + 1
66
- each_attempt = 0
67
- while True:
68
- try:
69
- gh_completion = subprocess.run(
70
- all_command,
71
- check=False,
72
- capture_output=True,
73
- text=True,
74
- timeout=timeout_seconds,
75
- )
76
- except subprocess.TimeoutExpired as error:
77
- error_stderr = _ensure_text(error.stderr)
78
- error_stdout = _ensure_text(error.stdout)
79
- message = (
80
- error_stderr or error_stdout or ""
81
- ).strip() or "gh command timed out"
82
- last_result = GhResult(
83
- returncode=GH_TIMEOUT_RETURN_CODE,
84
- stdout="",
85
- stderr=message,
86
- is_timed_out=True,
87
- )
88
- if each_attempt < max_attempts - 1:
89
- time.sleep(
90
- DEFAULT_BACKOFF_SECONDS
91
- * (EXPONENTIAL_BACKOFF_BASE**each_attempt)
92
- )
93
- each_attempt += 1
94
- continue
95
- return last_result
96
-
97
- gh_result = GhResult(
98
- returncode=gh_completion.returncode,
99
- stdout=gh_completion.stdout,
100
- stderr=gh_completion.stderr,
101
- )
102
- if gh_result.returncode == 0:
103
- return gh_result
104
-
105
- combined = f"{gh_result.stderr}\n{gh_result.stdout}".strip()
106
- if _is_auth_error(combined):
107
- return gh_result
108
- if each_attempt < max_attempts - 1 and _is_transient_error(combined):
109
- time.sleep(
110
- DEFAULT_BACKOFF_SECONDS * (EXPONENTIAL_BACKOFF_BASE**each_attempt)
111
- )
112
- each_attempt += 1
113
- continue
114
- return gh_result
115
-
116
-
117
- def fetch_inline_review_comments(
118
- owner: str,
119
- repo: str,
120
- pull_number: int,
121
- timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
122
- ) -> list[dict[str, object]] | None:
123
- """Fetch inline review comments for a pull request from the GitHub API.
124
-
125
- Returns the parsed list of comment objects on success, or None when the
126
- gh call fails or returns invalid/unexpected JSON. This preserves the
127
- distinction between "no inline comments" and "unable to determine
128
- inline comments".
129
- """
130
- api_path = INLINE_REVIEW_COMMENTS_PATH_TEMPLATE.format(
131
- owner=owner, repo=repo, pull_number=pull_number
132
- )
133
- fetch_result = run_gh(
134
- [
135
- "gh",
136
- "-R",
137
- f"{owner}/{repo}",
138
- "api",
139
- api_path,
140
- "--paginate",
141
- ],
142
- timeout_seconds=timeout_seconds,
143
- )
144
- if fetch_result.returncode != 0:
145
- return None
146
- parsed = _parse_paginated_json_array_documents(fetch_result.stdout)
147
- if parsed is None:
148
- return None
149
- if not all(isinstance(each_item, dict) for each_item in parsed):
150
- return None
151
- return parsed
152
-
153
-
154
- def _parse_paginated_json_array_documents(
155
- raw_output: str,
156
- ) -> list[dict[str, object]] | None:
157
- """Parse gh --paginate output that emits one JSON array per page.
158
-
159
- Concatenated array documents (`[...][...]`) are decoded one at a time
160
- using json.JSONDecoder.raw_decode and merged into a single flat list.
161
- Returns None when any decoded document is not a JSON array.
162
- """
163
- decoder = json.JSONDecoder()
164
- cursor_index = 0
165
- output_length = len(raw_output)
166
- flattened: list[dict[str, object]] = []
167
- while cursor_index < output_length:
168
- while cursor_index < output_length and raw_output[cursor_index].isspace():
169
- cursor_index += 1
170
- if cursor_index >= output_length:
171
- break
172
- try:
173
- decoded_document, end_index = decoder.raw_decode(
174
- raw_output, cursor_index
175
- )
176
- except json.JSONDecodeError:
177
- return None
178
- if not isinstance(decoded_document, list):
179
- return None
180
- flattened.extend(decoded_document)
181
- cursor_index = end_index
182
- return flattened
183
-
184
-
185
- def parse_owner_repo(repository: str) -> tuple[str, str]:
186
- if "/" not in repository:
187
- raise ValueError("repository must be owner/repo with exactly one slash")
188
- owner, name = repository.split("/", maxsplit=1)
189
- if not owner or not name:
190
- raise ValueError("repository must be owner/repo with exactly one slash")
191
- if "/" in name:
192
- raise ValueError("repository must be owner/repo with exactly one slash")
193
- return owner, name
@@ -1,257 +0,0 @@
1
- """Tests for shared gh_util.py promoted from babysit-pr/skills/babysit-prs/scripts/."""
2
-
3
- import importlib.util
4
- import inspect
5
- import subprocess
6
- import sys
7
- import unittest
8
- from pathlib import Path
9
- from types import ModuleType
10
- from unittest.mock import patch
11
-
12
-
13
- def _load_gh_util_module() -> ModuleType:
14
- module_path = Path(__file__).parent.parent / "gh_util.py"
15
- spec = importlib.util.spec_from_file_location("gh_util", module_path)
16
- assert spec is not None
17
- assert spec.loader is not None
18
- module = importlib.util.module_from_spec(spec)
19
- sys.modules["gh_util"] = module
20
- spec.loader.exec_module(module)
21
- return module
22
-
23
-
24
- gh_util = _load_gh_util_module()
25
-
26
-
27
- class ParseOwnerRepositoryTests(unittest.TestCase):
28
- def test_accepts_owner_repo(self) -> None:
29
- self.assertEqual(
30
- gh_util.parse_owner_repo("JonEcho/babysit-pr"),
31
- ("JonEcho", "babysit-pr"),
32
- )
33
-
34
- def test_rejects_missing_slash(self) -> None:
35
- with self.assertRaises(ValueError):
36
- gh_util.parse_owner_repo("babysit-pr")
37
-
38
- def test_rejects_empty_owner(self) -> None:
39
- with self.assertRaises(ValueError):
40
- gh_util.parse_owner_repo("/babysit-pr")
41
-
42
- def test_rejects_empty_name(self) -> None:
43
- with self.assertRaises(ValueError):
44
- gh_util.parse_owner_repo("JonEcho/")
45
-
46
- def test_rejects_extra_slash_segment(self) -> None:
47
- with self.assertRaises(ValueError):
48
- gh_util.parse_owner_repo("JonEcho/babysit-pr/extra")
49
-
50
-
51
- class RunGhTests(unittest.TestCase):
52
- def test_returns_on_first_success(self) -> None:
53
- success = subprocess.CompletedProcess(
54
- args=("gh",),
55
- returncode=0,
56
- stdout="ok",
57
- stderr="",
58
- )
59
- with patch.object(gh_util.subprocess, "run", return_value=success) as run_mock:
60
- with patch.object(gh_util.time, "sleep") as sleep_mock:
61
- result = gh_util.run_gh(("gh", "pr", "list"))
62
- self.assertEqual(result.returncode, 0)
63
- self.assertEqual(run_mock.call_count, 1)
64
- sleep_mock.assert_not_called()
65
-
66
- def test_retries_transient_failure_then_succeeds(self) -> None:
67
- failure = subprocess.CompletedProcess(
68
- args=("gh",),
69
- returncode=1,
70
- stdout="",
71
- stderr="connection reset by peer",
72
- )
73
- success = subprocess.CompletedProcess(
74
- args=("gh",),
75
- returncode=0,
76
- stdout="ok",
77
- stderr="",
78
- )
79
- with patch.object(
80
- gh_util.subprocess, "run", side_effect=[failure, success]
81
- ) as run_mock:
82
- with patch.object(gh_util.time, "sleep") as sleep_mock:
83
- result = gh_util.run_gh(("gh", "api", "ping"))
84
- self.assertEqual(result.returncode, 0)
85
- self.assertEqual(run_mock.call_count, 2)
86
- self.assertEqual(sleep_mock.call_count, 1)
87
-
88
- def test_does_not_retry_authentication_failure(self) -> None:
89
- failure = subprocess.CompletedProcess(
90
- args=("gh",),
91
- returncode=1,
92
- stdout="",
93
- stderr="HTTP 401: Bad credentials",
94
- )
95
- with patch.object(gh_util.subprocess, "run", return_value=failure) as run_mock:
96
- with patch.object(gh_util.time, "sleep") as sleep_mock:
97
- result = gh_util.run_gh(("gh", "pr", "view", "1"))
98
- self.assertEqual(result.returncode, 1)
99
- self.assertEqual(run_mock.call_count, 1)
100
- sleep_mock.assert_not_called()
101
-
102
-
103
- class FetchInlineReviewCommentsTests(unittest.TestCase):
104
- def test_returns_parsed_list_on_success(self) -> None:
105
- payload = [{"id": 1, "path": "foo.py", "line": 10, "body": "Fix this please"}]
106
- success = subprocess.CompletedProcess(
107
- args=("gh",),
108
- returncode=0,
109
- stdout='[{"id": 1, "path": "foo.py", "line": 10, "body": "Fix this please"}]',
110
- stderr="",
111
- )
112
- with patch.object(gh_util.subprocess, "run", return_value=success):
113
- result = gh_util.fetch_inline_review_comments("JonEcho", "babysit-pr", 17)
114
- self.assertEqual(result, payload)
115
-
116
- def test_returns_none_on_gh_failure(self) -> None:
117
- failure = subprocess.CompletedProcess(
118
- args=("gh",),
119
- returncode=1,
120
- stdout="",
121
- stderr="Not Found",
122
- )
123
- with patch.object(gh_util.subprocess, "run", return_value=failure):
124
- result = gh_util.fetch_inline_review_comments("JonEcho", "babysit-pr", 17)
125
- self.assertIsNone(result)
126
-
127
- def test_returns_none_on_invalid_json_with_success_returncode(self) -> None:
128
- success_with_invalid_json = subprocess.CompletedProcess(
129
- args=("gh",),
130
- returncode=0,
131
- stdout="not valid json",
132
- stderr="",
133
- )
134
- with patch.object(
135
- gh_util.subprocess, "run", return_value=success_with_invalid_json
136
- ):
137
- result = gh_util.fetch_inline_review_comments("JonEcho", "babysit-pr", 17)
138
- self.assertIsNone(result)
139
-
140
- def test_returns_none_when_response_is_not_a_list(self) -> None:
141
- success_with_object = subprocess.CompletedProcess(
142
- args=("gh",),
143
- returncode=0,
144
- stdout='{"message": "unexpected object"}',
145
- stderr="",
146
- )
147
- with patch.object(gh_util.subprocess, "run", return_value=success_with_object):
148
- result = gh_util.fetch_inline_review_comments("JonEcho", "babysit-pr", 17)
149
- self.assertIsNone(result)
150
-
151
- def test_returns_none_when_inner_items_are_not_dicts(self) -> None:
152
- success_with_non_dict_items = subprocess.CompletedProcess(
153
- args=("gh",),
154
- returncode=0,
155
- stdout="[1, 2, 3]",
156
- stderr="",
157
- )
158
- with patch.object(
159
- gh_util.subprocess, "run", return_value=success_with_non_dict_items
160
- ):
161
- result = gh_util.fetch_inline_review_comments("JonEcho", "babysit-pr", 17)
162
- self.assertIsNone(result)
163
-
164
- def test_returns_none_when_inner_items_mix_dict_and_string(self) -> None:
165
- success_with_mixed_items = subprocess.CompletedProcess(
166
- args=("gh",),
167
- returncode=0,
168
- stdout='[{"id": 1, "path": "a.py"}, "stray string"]',
169
- stderr="",
170
- )
171
- with patch.object(
172
- gh_util.subprocess, "run", return_value=success_with_mixed_items
173
- ):
174
- result = gh_util.fetch_inline_review_comments("JonEcho", "babysit-pr", 17)
175
- self.assertIsNone(result)
176
-
177
-
178
- class RunGhUnreachableAssertionRemovedTests(unittest.TestCase):
179
- def test_run_gh_function_body_does_not_contain_unreachable_assertion(self) -> None:
180
- run_gh_source_text = inspect.getsource(gh_util.run_gh)
181
- assert "AssertionError" not in run_gh_source_text
182
-
183
-
184
- class RunGhUnreachableTrailingReturnRemovedTests(unittest.TestCase):
185
- """Regression: every for-loop branch in run_gh returns a GhResult, so the
186
- trailing `return GhResult(...)` block after the loop is unreachable. The
187
- body must terminate with the for-loop's own returns, not a fallback block
188
- referencing 'exhausted all attempts'.
189
- """
190
-
191
- def test_run_gh_function_body_does_not_contain_unreachable_trailing_return(
192
- self,
193
- ) -> None:
194
- run_gh_source_text = inspect.getsource(gh_util.run_gh)
195
- assert "exhausted all attempts" not in run_gh_source_text
196
-
197
-
198
- class EnsureTextParameterNameTests(unittest.TestCase):
199
- """The `_ensure_text` parameter must not use the banned name `value`.
200
-
201
- CODE_RULES.md §5 bans generic names like `value`. The parameter must
202
- describe what it carries -- a subprocess stdout/stderr field that may be
203
- str, bytes, or None.
204
- """
205
-
206
- def test_ensure_text_parameter_is_not_named_value(self) -> None:
207
- ensure_text_signature = inspect.signature(gh_util._ensure_text)
208
- all_parameter_names = list(ensure_text_signature.parameters)
209
- assert "value" not in all_parameter_names
210
-
211
-
212
- class FetchInlineReviewCommentsPaginationTests(unittest.TestCase):
213
- """Regression: gh --paginate emits one JSON document per page concatenated.
214
-
215
- Per the project's gh-paginate rule, --paginate for a list endpoint emits
216
- one JSON array per page (e.g. `[...]\\n[...]\\n[...]`), and json.loads
217
- fails on the second `[`. fetch_inline_review_comments must merge those
218
- documents and return the flattened list rather than returning None.
219
- """
220
-
221
- def test_returns_flattened_list_for_concatenated_pages(self) -> None:
222
- page_one_body = '[{"id": 1, "path": "a.py"}, {"id": 2, "path": "b.py"}]'
223
- page_two_body = '[{"id": 3, "path": "c.py"}]'
224
- concatenated_pages = f"{page_one_body}\n{page_two_body}\n"
225
- success = subprocess.CompletedProcess(
226
- args=("gh",),
227
- returncode=0,
228
- stdout=concatenated_pages,
229
- stderr="",
230
- )
231
- with patch.object(gh_util.subprocess, "run", return_value=success):
232
- fetched_comments = gh_util.fetch_inline_review_comments(
233
- "JonEcho", "babysit-pr", 17
234
- )
235
- assert fetched_comments == [
236
- {"id": 1, "path": "a.py"},
237
- {"id": 2, "path": "b.py"},
238
- {"id": 3, "path": "c.py"},
239
- ]
240
-
241
- def test_returns_none_when_concatenated_page_is_not_a_list(self) -> None:
242
- concatenated_with_object = '[{"id": 1}]\n{"message": "oops"}\n'
243
- success = subprocess.CompletedProcess(
244
- args=("gh",),
245
- returncode=0,
246
- stdout=concatenated_with_object,
247
- stderr="",
248
- )
249
- with patch.object(gh_util.subprocess, "run", return_value=success):
250
- fetched_comments = gh_util.fetch_inline_review_comments(
251
- "JonEcho", "babysit-pr", 17
252
- )
253
- assert fetched_comments is None
254
-
255
-
256
- if __name__ == "__main__":
257
- unittest.main()
@@ -1,61 +0,0 @@
1
- """Tests for gh_util_constants.py extracted constant set."""
2
-
3
- import importlib.util
4
- from pathlib import Path
5
- from types import ModuleType
6
-
7
-
8
- def _load_constants_module() -> ModuleType:
9
- module_path = Path(__file__).parent.parent / "config" / "gh_util_constants.py"
10
- specification = importlib.util.spec_from_file_location(
11
- "config.gh_util_constants", module_path
12
- )
13
- assert specification is not None
14
- assert specification.loader is not None
15
- module = importlib.util.module_from_spec(specification)
16
- specification.loader.exec_module(module)
17
- return module
18
-
19
-
20
- constants_module = _load_constants_module()
21
-
22
-
23
- def test_default_timeout_seconds_is_typed_integer() -> None:
24
- assert isinstance(constants_module.DEFAULT_TIMEOUT_SECONDS, int)
25
- assert constants_module.DEFAULT_TIMEOUT_SECONDS == 30
26
-
27
-
28
- def test_default_retries_is_typed_integer() -> None:
29
- assert isinstance(constants_module.DEFAULT_RETRIES, int)
30
- assert constants_module.DEFAULT_RETRIES == 2
31
-
32
-
33
- def test_default_backoff_seconds_is_typed_float() -> None:
34
- assert isinstance(constants_module.DEFAULT_BACKOFF_SECONDS, float)
35
-
36
-
37
- def test_exponential_backoff_base_is_typed_integer() -> None:
38
- assert isinstance(constants_module.EXPONENTIAL_BACKOFF_BASE, int)
39
- assert constants_module.EXPONENTIAL_BACKOFF_BASE == 2
40
-
41
-
42
- def test_gh_timeout_return_code_is_typed_integer() -> None:
43
- assert isinstance(constants_module.GH_TIMEOUT_RETURN_CODE, int)
44
- assert constants_module.GH_TIMEOUT_RETURN_CODE == 124
45
-
46
-
47
- def test_inline_review_comments_path_template_renders() -> None:
48
- rendered = constants_module.INLINE_REVIEW_COMMENTS_PATH_TEMPLATE.format(
49
- owner="acme", repo="lib", pull_number=7
50
- )
51
- assert rendered == "/repos/acme/lib/pulls/7/comments"
52
-
53
-
54
- def test_all_transient_error_markers_is_tuple() -> None:
55
- assert isinstance(constants_module.ALL_TRANSIENT_ERROR_MARKERS, tuple)
56
- assert "timeout" in constants_module.ALL_TRANSIENT_ERROR_MARKERS
57
-
58
-
59
- def test_all_auth_error_markers_is_tuple() -> None:
60
- assert isinstance(constants_module.ALL_AUTH_ERROR_MARKERS, tuple)
61
- assert "authentication failed" in constants_module.ALL_AUTH_ERROR_MARKERS
@@ -1,78 +0,0 @@
1
- """Resolve the per-tick mergeability state of an explicitly-targeted PR.
2
-
3
- Wraps ``gh pr view <number> --repo <owner>/<repo> --json mergeable,mergeStateStatus,headRefOid``
4
- so the skill body emits one script invocation. Single-object endpoint - no
5
- pagination. Explicit ``--owner``/``--repo``/``--number`` targeting matches every
6
- sibling convergence-gate script (``fetch_*_reviews.py``,
7
- ``fetch_*_inline_comments.py``, ``request_copilot_review.py``,
8
- ``mark_pr_ready.py``); under multi-PR orchestration the gate is guaranteed
9
- to query the intended PR rather than whichever PR the current git context
10
- points at.
11
-
12
- The returned dict gates pr-converge's mark-ready step against PRs whose base
13
- branch state is DIRTY (conflicts) or otherwise non-CLEAN.
14
- """
15
-
16
- import argparse
17
- import json
18
- import subprocess
19
- import sys
20
- from pathlib import Path
21
-
22
- if str(Path(__file__).resolve().parent) not in sys.path:
23
- sys.path.insert(0, str(Path(__file__).resolve().parent))
24
-
25
- from evict_cached_config_modules import evict_cached_config_modules
26
-
27
- evict_cached_config_modules()
28
-
29
- from config.pr_converge_constants import GH_REPO_ARG_TEMPLATE, MERGEABILITY_FIELDS
30
-
31
-
32
- def check_pr_mergeability(
33
- *,
34
- owner: str,
35
- repo: str,
36
- number: int,
37
- ) -> dict[str, object]:
38
- """Return ``{mergeable, mergeStateStatus, headRefOid}`` from ``gh pr view`` for the targeted PR."""
39
- repo_arg = GH_REPO_ARG_TEMPLATE.format(owner=owner, repo=repo)
40
- gh_command: list[str] = [
41
- "gh",
42
- "pr",
43
- "view",
44
- str(number),
45
- "--repo",
46
- repo_arg,
47
- "--json",
48
- MERGEABILITY_FIELDS,
49
- ]
50
- completed = subprocess.run(
51
- gh_command,
52
- capture_output=True,
53
- check=True,
54
- text=True,
55
- encoding="utf-8",
56
- errors="replace",
57
- )
58
- return json.loads(completed.stdout)
59
-
60
-
61
- def main() -> int:
62
- parser = argparse.ArgumentParser(description=__doc__)
63
- parser.add_argument("--owner", required=True)
64
- parser.add_argument("--repo", required=True)
65
- parser.add_argument("--number", required=True, type=int)
66
- parsed_arguments = parser.parse_args()
67
- mergeability_state = check_pr_mergeability(
68
- owner=parsed_arguments.owner,
69
- repo=parsed_arguments.repo,
70
- number=parsed_arguments.number,
71
- )
72
- json.dump(mergeability_state, sys.stdout)
73
- sys.stdout.write("\n")
74
- return 0
75
-
76
-
77
- if __name__ == "__main__":
78
- sys.exit(main())