claude-dev-env 1.36.1 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/audit-contract.md +159 -0
- package/_shared/pr-loop/code-rules-gate.md +64 -0
- package/_shared/pr-loop/fix-protocol.md +37 -0
- package/_shared/pr-loop/gh-payloads.md +85 -0
- package/_shared/pr-loop/scripts/README.md +20 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +234 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +975 -0
- package/_shared/pr-loop/scripts/config/__init__.py +0 -0
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +11 -0
- package/_shared/pr-loop/scripts/config/code_rules_gate_constants.py +56 -0
- package/_shared/pr-loop/scripts/config/fix_hookspath_constants.py +25 -0
- package/_shared/pr-loop/scripts/config/gh_util_constants.py +31 -0
- package/_shared/pr-loop/scripts/config/preflight_constants.py +68 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +260 -0
- package/_shared/pr-loop/scripts/gh_util.py +193 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +130 -0
- package/_shared/pr-loop/scripts/preflight.py +449 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +156 -0
- package/_shared/pr-loop/scripts/tests/conftest.py +51 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +135 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +169 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +58 -0
- package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +50 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +917 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +102 -0
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +374 -0
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +47 -0
- package/_shared/pr-loop/scripts/tests/test_gh_util.py +257 -0
- package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +61 -0
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +49 -0
- package/_shared/pr-loop/scripts/tests/test_preflight.py +670 -0
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +77 -0
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +49 -0
- package/_shared/pr-loop/state-schema.md +81 -0
- package/hooks/blocking/code_rules_enforcer.py +269 -23
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
- package/hooks/config/test_unused_module_import_constants.py +48 -0
- package/hooks/config/unused_module_import_constants.py +41 -0
- package/package.json +2 -1
- package/skills/bg-agent/SKILL.md +69 -0
- package/skills/bugteam/CONSTRAINTS.md +10 -19
- package/skills/bugteam/PROMPTS.md +3 -3
- package/skills/bugteam/SKILL.md +103 -202
- package/skills/bugteam/SKILL_EVALS.md +75 -114
- package/skills/bugteam/reference/README.md +2 -4
- package/skills/bugteam/reference/design-rationale.md +3 -8
- package/skills/bugteam/reference/team-setup.md +11 -19
- package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
- package/skills/bugteam/scripts/config/__init__.py +0 -0
- package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
- package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
- package/skills/bugteam/sources.md +1 -25
- package/skills/bugteam/test_skill_additions.py +4 -13
- package/skills/fresh-branch/SKILL.md +71 -0
- package/skills/gotcha/SKILL.md +73 -0
- package/skills/monitor-open-prs/SKILL.md +4 -37
- package/skills/monitor-open-prs/test_skill_contract.py +0 -5
- package/skills/pr-converge/SKILL.md +60 -1298
- package/skills/pr-converge/reference/convergence-gates.md +118 -0
- package/skills/pr-converge/reference/examples.md +76 -0
- package/skills/pr-converge/reference/fix-protocol.md +54 -0
- package/skills/pr-converge/reference/ground-rules.md +13 -0
- package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
- package/skills/pr-converge/reference/per-tick.md +201 -0
- package/skills/pr-converge/reference/state-schema.md +19 -0
- package/skills/pr-converge/reference/stop-conditions.md +26 -0
- package/skills/pr-converge/scripts/README.md +36 -9
- package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +58 -5
- package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
- package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
- package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
- package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
- package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
- package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
- package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
- package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
- package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
- package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
- package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
- package/skills/bugteam/test_team_lifecycle.py +0 -103
- package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
- package/skills/pr-converge/test_team_lifecycle.py +0 -56
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +0 -108
|
@@ -0,0 +1,257 @@
|
|
|
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()
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Smoke tests for grant_project_claude_permissions wiring.
|
|
2
|
+
|
|
3
|
+
Confirms the module imports cleanly with the constants now sourced from
|
|
4
|
+
config/claude_permissions_constants.py and config/claude_settings_keys_constants.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_grant_module() -> ModuleType:
|
|
16
|
+
scripts_directory = Path(__file__).parent.parent
|
|
17
|
+
parent_directory = str(scripts_directory.resolve())
|
|
18
|
+
if parent_directory not in sys.path:
|
|
19
|
+
sys.path.insert(0, parent_directory)
|
|
20
|
+
sys.modules.pop("config", None)
|
|
21
|
+
module_path = scripts_directory / "grant_project_claude_permissions.py"
|
|
22
|
+
specification = importlib.util.spec_from_file_location(
|
|
23
|
+
"grant_project_claude_permissions", module_path
|
|
24
|
+
)
|
|
25
|
+
assert specification is not None
|
|
26
|
+
assert specification.loader is not None
|
|
27
|
+
module = importlib.util.module_from_spec(specification)
|
|
28
|
+
specification.loader.exec_module(module)
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_module_imports_constants_from_config_modules() -> None:
|
|
33
|
+
grant_module = _load_grant_module()
|
|
34
|
+
assert grant_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Write", "Read")
|
|
35
|
+
assert "{project_path}" in grant_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
|
|
36
|
+
assert grant_module.CLAUDE_SETTINGS_PERMISSIONS_KEY == "permissions"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_grant_module_guards_sys_path_insert_against_duplicates() -> None:
|
|
40
|
+
"""grant_project_claude_permissions.py must guard its sys.path.insert with a
|
|
41
|
+
membership check so re-imports under test harnesses do not push duplicate
|
|
42
|
+
entries (matching the pattern used by every other module in the directory)."""
|
|
43
|
+
module_source = (
|
|
44
|
+
Path(__file__).parent.parent / "grant_project_claude_permissions.py"
|
|
45
|
+
).read_text(encoding="utf-8")
|
|
46
|
+
assert "if str(Path(__file__).resolve().parent) not in sys.path:" in module_source, (
|
|
47
|
+
"grant_project_claude_permissions.py must guard sys.path.insert against "
|
|
48
|
+
"duplicate entries on reload (consistent with sibling modules)"
|
|
49
|
+
)
|