claude-dev-env 1.40.0 → 1.42.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/CLAUDE.md +9 -1
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +231 -3
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +56 -2
- package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +2 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +173 -6
- package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +135 -14
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +33 -0
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +4 -2
- package/hooks/_gh_pr_author_swap_utils.py +1211 -0
- package/hooks/blocking/gh_body_arg_blocker.py +9 -6
- package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
- package/hooks/blocking/gh_pr_author_restore.py +100 -0
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
- package/hooks/blocking/pr_description_enforcer.py +1 -3
- package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
- package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
- package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
- package/hooks/config/gh_pr_author_swap_constants.py +76 -0
- package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
- package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
- package/hooks/config/pr_description_enforcer_constants.py +5 -0
- package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
- package/hooks/hooks.json +40 -0
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
- package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
- package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
- package/hooks/test__gh_pr_author_swap_utils.py +333 -0
- package/package.json +1 -1
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
- package/skills/bugteam/reference/audit-contract.md +22 -0
- package/skills/bugteam/reference/github-pr-reviews.md +1 -1
- package/skills/bugteam/scripts/_claude_permissions_common.py +109 -0
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +51 -2
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +115 -4
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +69 -17
- package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
- package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
- package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
- package/skills/implement/SKILL.md +66 -0
- package/skills/implement/scripts/append_note.py +133 -0
- package/skills/implement/scripts/config/__init__.py +0 -0
- package/skills/implement/scripts/config/notes_constants.py +12 -0
- package/skills/implement/scripts/test_append_note.py +191 -0
- package/skills/pr-converge/SKILL.md +8 -2
- package/skills/pr-converge/config/constants.py +7 -1
- package/skills/pr-converge/reference/state-schema.md +36 -8
- package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/check_convergence.py +167 -28
- package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
- package/skills/pr-converge/scripts/conftest.py +60 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
- package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/test_check_convergence.py +306 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +1 -1
- package/skills/refine/SKILL.md +257 -0
- package/skills/refine/templates/implementation-notes-template.html +56 -0
- package/skills/refine/templates/plan-template.md +60 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
"""Unit tests for the shared gh-pr-author swap utils module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import pathlib
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Iterator
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
_HOOKS_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
16
|
+
for each_sys_path_entry in (str(_HOOKS_ROOT), str(_HOOKS_ROOT / "blocking")):
|
|
17
|
+
if each_sys_path_entry not in sys.path:
|
|
18
|
+
sys.path.insert(0, each_sys_path_entry)
|
|
19
|
+
|
|
20
|
+
utils_module_spec = importlib.util.spec_from_file_location(
|
|
21
|
+
"_gh_pr_author_swap_utils",
|
|
22
|
+
_HOOKS_ROOT / "_gh_pr_author_swap_utils.py",
|
|
23
|
+
)
|
|
24
|
+
assert utils_module_spec is not None
|
|
25
|
+
assert utils_module_spec.loader is not None
|
|
26
|
+
utils_module = importlib.util.module_from_spec(utils_module_spec)
|
|
27
|
+
utils_module_spec.loader.exec_module(utils_module)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def isolated_temp_directory(
|
|
32
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
33
|
+
tmp_path: pathlib.Path,
|
|
34
|
+
) -> Iterator[pathlib.Path]:
|
|
35
|
+
monkeypatch.setattr(utils_module.tempfile, "gettempdir", lambda: str(tmp_path))
|
|
36
|
+
yield tmp_path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_strip_quoted_regions_preserves_offsets_for_double_quotes() -> None:
|
|
40
|
+
original_command = 'gh pr create --body "some text" --title T'
|
|
41
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
42
|
+
assert len(stripped_command) == len(original_command)
|
|
43
|
+
assert "some text" not in stripped_command
|
|
44
|
+
assert "gh pr create" in stripped_command
|
|
45
|
+
assert "--title T" in stripped_command
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_strip_quoted_regions_preserves_offsets_for_single_quotes() -> None:
|
|
49
|
+
original_command = "gh pr create --body 'single quoted body' --title T"
|
|
50
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
51
|
+
assert len(stripped_command) == len(original_command)
|
|
52
|
+
assert "single quoted body" not in stripped_command
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_strip_quoted_regions_preserves_backtick_substitution_body() -> None:
|
|
56
|
+
"""Backticks delimit command substitution, which executes — the body must remain scannable."""
|
|
57
|
+
original_command = "echo `inner cmd` && gh pr create --title T"
|
|
58
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
59
|
+
assert len(stripped_command) == len(original_command)
|
|
60
|
+
assert "inner cmd" in stripped_command
|
|
61
|
+
assert "gh pr create" in stripped_command
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_strip_quoted_regions_preserves_dollar_paren_substitution_body() -> None:
|
|
65
|
+
"""``$(...)`` substitution body must remain scannable for the same reason as backticks."""
|
|
66
|
+
original_command = "echo $(inner cmd) && gh pr create --title T"
|
|
67
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
68
|
+
assert len(stripped_command) == len(original_command)
|
|
69
|
+
assert "inner cmd" in stripped_command
|
|
70
|
+
assert "gh pr create" in stripped_command
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_strip_quoted_regions_preserves_dollar_paren_inside_double_quotes() -> None:
|
|
74
|
+
"""``"$(...)"`` substitution body remains scannable even when wrapped in double quotes."""
|
|
75
|
+
original_command = 'echo "$(inner cmd)" && gh pr create --title T'
|
|
76
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
77
|
+
assert len(stripped_command) == len(original_command)
|
|
78
|
+
assert "inner cmd" in stripped_command
|
|
79
|
+
assert "gh pr create" in stripped_command
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_strip_quoted_regions_preserves_backtick_substitution_inside_double_quotes() -> None:
|
|
83
|
+
"""`gh pr create` inside a backtick substitution inside double quotes stays scannable."""
|
|
84
|
+
original_command = 'echo "`gh pr create --title T`"'
|
|
85
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
86
|
+
assert "gh pr create" in stripped_command
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_strip_quoted_regions_handles_escaped_quote_inside_double_quotes() -> None:
|
|
90
|
+
original_command = 'gh pr create --body "escaped \\" quote" --title T'
|
|
91
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
92
|
+
assert len(stripped_command) == len(original_command)
|
|
93
|
+
assert "escaped" not in stripped_command
|
|
94
|
+
assert "--title T" in stripped_command
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_strip_quoted_regions_returns_empty_for_empty_input() -> None:
|
|
98
|
+
assert utils_module._strip_quoted_regions("") == ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_strip_quoted_regions_leaves_unquoted_command_unchanged() -> None:
|
|
102
|
+
unquoted_command = "gh pr create --title T --body-file body.md"
|
|
103
|
+
assert utils_module._strip_quoted_regions(unquoted_command) == unquoted_command
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_strip_quoted_regions_handles_unterminated_quote_to_end() -> None:
|
|
107
|
+
unterminated_command = 'gh pr create --body "never closed gh pr create'
|
|
108
|
+
stripped_command = utils_module._strip_quoted_regions(unterminated_command)
|
|
109
|
+
assert len(stripped_command) == len(unterminated_command)
|
|
110
|
+
assert "never closed" not in stripped_command
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_command_invokes_gh_pr_create_matches_basic_form() -> None:
|
|
114
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
115
|
+
utils_module._strip_quoted_regions("gh pr create --title T")
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_command_invokes_gh_pr_create_matches_chained_form() -> None:
|
|
120
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
121
|
+
utils_module._strip_quoted_regions("git push && gh pr create")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_command_invokes_gh_pr_create_rejects_pr_edit() -> None:
|
|
126
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
127
|
+
utils_module._strip_quoted_regions("gh pr edit 10 --title X")
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_command_invokes_gh_pr_create_rejects_substring() -> None:
|
|
132
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
133
|
+
utils_module._strip_quoted_regions("some-gh pr created-by")
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_command_invokes_gh_pr_create_ignores_literal_inside_double_quotes() -> None:
|
|
138
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
139
|
+
utils_module._strip_quoted_regions('echo "gh pr create docs"')
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_command_invokes_gh_pr_create_ignores_literal_inside_single_quotes() -> None:
|
|
144
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
145
|
+
utils_module._strip_quoted_regions("echo 'gh pr create docs'")
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_command_invokes_gh_pr_create_detects_backtick_substitution_body() -> None:
|
|
150
|
+
"""Backtick substitution body executes, so an inner ``gh pr create`` is real."""
|
|
151
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
152
|
+
utils_module._strip_quoted_regions("echo `gh pr create docs`")
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_command_invokes_gh_pr_create_detects_dollar_paren_substitution_body() -> None:
|
|
157
|
+
"""``$(...)`` substitution body executes, so an inner ``gh pr create`` is real."""
|
|
158
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
159
|
+
utils_module._strip_quoted_regions('echo "$(gh pr create docs)"')
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_command_invokes_gh_pr_create_still_matches_unquoted_invocation() -> None:
|
|
164
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
165
|
+
utils_module._strip_quoted_regions(
|
|
166
|
+
'gh pr create --body "see docs about gh pr create"'
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_strip_quoted_regions_balances_paren_inside_double_quoted_substitution_body() -> None:
|
|
172
|
+
"""A ``)`` inside ``"..."`` within ``$(...)`` must not prematurely close the substitution."""
|
|
173
|
+
original_command = 'echo $(echo ")") && gh pr create --title T'
|
|
174
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
175
|
+
assert len(stripped_command) == len(original_command)
|
|
176
|
+
assert "gh pr create" in stripped_command
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_command_invokes_gh_pr_create_detects_real_invocation_after_double_quoted_paren_in_substitution() -> None:
|
|
180
|
+
"""The real ``gh pr create`` after a ``$(echo ")")`` block must still be detected."""
|
|
181
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
182
|
+
utils_module._strip_quoted_regions('echo $(echo ")") && gh pr create --title T')
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_strip_quoted_regions_balances_paren_inside_single_quoted_substitution_body() -> None:
|
|
187
|
+
"""A ``)`` inside ``'...'`` within ``$(...)`` must not prematurely close the substitution."""
|
|
188
|
+
original_command = "echo $(echo ')') && gh pr create --title T"
|
|
189
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
190
|
+
assert len(stripped_command) == len(original_command)
|
|
191
|
+
assert "gh pr create" in stripped_command
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_command_invokes_gh_pr_create_detects_real_invocation_after_single_quoted_paren_in_substitution() -> None:
|
|
195
|
+
"""The real ``gh pr create`` after a ``$(echo ')')`` block must still be detected."""
|
|
196
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
197
|
+
utils_module._strip_quoted_regions("echo $(echo ')') && gh pr create --title T")
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_command_invokes_gh_pr_create_detects_real_invocation_after_escaped_quote_in_substitution() -> None:
|
|
202
|
+
"""A ``\\"`` inside ``"..."`` in ``$(...)`` does not close the quoted region; balance still holds."""
|
|
203
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
204
|
+
utils_module._strip_quoted_regions('echo $(echo "\\")") && gh pr create --title T')
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_command_invokes_gh_pr_create_detects_real_invocation_after_backtick_paren_in_substitution() -> None:
|
|
209
|
+
"""A ``)`` inside ``` `...` ``` within ``$(...)`` must not prematurely close the substitution."""
|
|
210
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
211
|
+
utils_module._strip_quoted_regions("echo $(echo `foo)bar`) && gh pr create --title T")
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_command_invokes_gh_pr_create_detects_real_invocation_after_subshell_in_substitution() -> None:
|
|
216
|
+
"""A bash subshell ``(echo b)`` inside ``$(...)`` must not prematurely close the outer substitution."""
|
|
217
|
+
command = '''echo "$(printf 'before'; (echo nested); printf 'after')" && gh pr create --title T'''
|
|
218
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
219
|
+
utils_module._strip_quoted_regions(command)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_command_invokes_gh_pr_create_detects_real_invocation_after_array_in_substitution() -> None:
|
|
224
|
+
"""A bash array assignment ``arr=(a b c)`` inside ``$(...)`` must not prematurely close the outer substitution."""
|
|
225
|
+
command = '''echo "$(arr=(a b c); echo "${arr[@]}")" && gh pr create --title T'''
|
|
226
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
227
|
+
utils_module._strip_quoted_regions(command)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_command_invokes_gh_pr_create_detects_real_invocation_after_function_in_substitution() -> None:
|
|
232
|
+
"""A bash function definition ``f() { ... }`` inside ``$(...)`` must not prematurely close the outer substitution."""
|
|
233
|
+
command = '''echo "$(f() { echo z; }; f)" && gh pr create --title T'''
|
|
234
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
235
|
+
utils_module._strip_quoted_regions(command)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_command_invokes_gh_pr_create_detects_invocation_after_nested_substitution_in_double_quoted_region() -> None:
|
|
240
|
+
"""A ``$(...)`` nested inside a ``"..."`` inside an outer ``$(...)`` must not flip the outer quoted boundary."""
|
|
241
|
+
command = '''echo "$(echo "$(echo "deeply nested")")" && gh pr create --title T'''
|
|
242
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
243
|
+
utils_module._strip_quoted_regions(command)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_command_invokes_gh_pr_create_detects_invocation_after_backtick_substitution_in_double_quoted_region() -> None:
|
|
248
|
+
"""A ``` `...` ``` nested inside a ``"..."`` inside an outer ``$(...)`` must not flip the outer quoted boundary."""
|
|
249
|
+
command = '''echo "$(echo "`echo nested`")" && gh pr create --title T'''
|
|
250
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
251
|
+
utils_module._strip_quoted_regions(command)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_command_invokes_gh_pr_create_rejects_newline_between_pr_and_create() -> None:
|
|
256
|
+
"""``gh pr\\ncreate-report.sh`` is two commands; the second is not ``create``."""
|
|
257
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
258
|
+
utils_module._strip_quoted_regions("gh pr\ncreate-report.sh")
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_command_invokes_gh_pr_create_matches_tab_separated_tokens() -> None:
|
|
263
|
+
"""Tab characters between ``gh``, ``pr``, and ``create`` still match the invocation pattern."""
|
|
264
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
265
|
+
utils_module._strip_quoted_regions("gh\tpr\tcreate --title T")
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_command_invokes_gh_pr_create_matches_short_repo_flag() -> None:
|
|
270
|
+
"""``gh -R owner/repo pr create`` must match — the short repo flag separates ``gh`` from ``pr``."""
|
|
271
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
272
|
+
utils_module._strip_quoted_regions("gh -R foo/bar pr create --title T")
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_command_invokes_gh_pr_create_matches_long_repo_flag_with_space() -> None:
|
|
277
|
+
"""``gh --repo owner/repo pr create`` must match — space-separated long flag plus value."""
|
|
278
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
279
|
+
utils_module._strip_quoted_regions("gh --repo foo/bar pr create --title T")
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_command_invokes_gh_pr_create_matches_long_repo_flag_with_equals() -> None:
|
|
284
|
+
"""``gh --repo=owner/repo pr create`` must match — equals-attached long flag value."""
|
|
285
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
286
|
+
utils_module._strip_quoted_regions("gh --repo=foo/bar pr create --title T")
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_command_invokes_gh_pr_create_matches_multiple_intervening_flags() -> None:
|
|
291
|
+
"""Multiple top-level flags between ``gh`` and ``pr create`` must all be tolerated."""
|
|
292
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
293
|
+
utils_module._strip_quoted_regions("gh -R foo/bar --hostname github.com pr create")
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_command_invokes_gh_pr_create_rejects_gh_dash_pr_create() -> None:
|
|
298
|
+
"""``gh-pr-create`` is a single hyphenated token, not an invocation of ``gh pr create``."""
|
|
299
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
300
|
+
utils_module._strip_quoted_regions("gh-pr-create --foo")
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_command_invokes_gh_pr_create_still_matches_basic_form() -> None:
|
|
305
|
+
"""Regression — the original ``gh pr create`` form must continue to match after pattern widening."""
|
|
306
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
307
|
+
utils_module._strip_quoted_regions("gh pr create --title T")
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_state_file_path_uses_session_id(
|
|
312
|
+
isolated_temp_directory: pathlib.Path,
|
|
313
|
+
) -> None:
|
|
314
|
+
state_file = utils_module._state_file_path("abc-123")
|
|
315
|
+
assert state_file.parent == isolated_temp_directory
|
|
316
|
+
assert state_file.name == "gh_pr_author_swap_abc-123.json"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_state_file_path_falls_back_to_default_when_session_id_empty(
|
|
320
|
+
isolated_temp_directory: pathlib.Path,
|
|
321
|
+
) -> None:
|
|
322
|
+
state_file = utils_module._state_file_path("")
|
|
323
|
+
assert state_file.parent == isolated_temp_directory
|
|
324
|
+
assert state_file.name == "gh_pr_author_swap_default.json"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_state_file_path_includes_default_for_falsy_input(
|
|
328
|
+
isolated_temp_directory: pathlib.Path,
|
|
329
|
+
) -> None:
|
|
330
|
+
state_file_for_empty_string = utils_module._state_file_path("")
|
|
331
|
+
assert "default" in state_file_for_empty_string.name
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_switch_gh_account_returns_true_on_success() -> None:
|
|
335
|
+
completed = mock.Mock(returncode=0, stdout="", stderr="")
|
|
336
|
+
with mock.patch.object(utils_module.subprocess, "run", return_value=completed):
|
|
337
|
+
assert utils_module._switch_gh_account("JonEcho") is True
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_switch_gh_account_returns_false_on_nonzero_exit() -> None:
|
|
341
|
+
completed = mock.Mock(returncode=1, stdout="", stderr="boom")
|
|
342
|
+
with mock.patch.object(utils_module.subprocess, "run", return_value=completed):
|
|
343
|
+
assert utils_module._switch_gh_account("JonEcho") is False
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_switch_gh_account_returns_false_when_gh_missing() -> None:
|
|
347
|
+
with mock.patch.object(utils_module.subprocess, "run", side_effect=FileNotFoundError):
|
|
348
|
+
assert utils_module._switch_gh_account("JonEcho") is False
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_switch_gh_account_returns_false_on_timeout() -> None:
|
|
352
|
+
with mock.patch.object(
|
|
353
|
+
utils_module.subprocess,
|
|
354
|
+
"run",
|
|
355
|
+
side_effect=utils_module.subprocess.TimeoutExpired(cmd="gh", timeout=10),
|
|
356
|
+
):
|
|
357
|
+
assert utils_module._switch_gh_account("JonEcho") is False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_read_original_account_returns_login_for_well_formed_file(
|
|
361
|
+
isolated_temp_directory: pathlib.Path,
|
|
362
|
+
) -> None:
|
|
363
|
+
state_file = isolated_temp_directory / "well_formed.json"
|
|
364
|
+
state_file.write_text(
|
|
365
|
+
json.dumps({"original_account": "jl-cmd", "primary_account": "JonEcho"}),
|
|
366
|
+
encoding="utf-8",
|
|
367
|
+
)
|
|
368
|
+
assert utils_module._read_original_account(state_file) == "jl-cmd"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_read_original_account_returns_none_for_missing_file(
|
|
372
|
+
isolated_temp_directory: pathlib.Path,
|
|
373
|
+
) -> None:
|
|
374
|
+
missing_file = isolated_temp_directory / "does_not_exist.json"
|
|
375
|
+
assert utils_module._read_original_account(missing_file) is None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_read_original_account_returns_none_for_non_dict_payload(
|
|
379
|
+
isolated_temp_directory: pathlib.Path,
|
|
380
|
+
) -> None:
|
|
381
|
+
list_payload_file = isolated_temp_directory / "list_payload.json"
|
|
382
|
+
list_payload_file.write_text(json.dumps(["jl-cmd"]), encoding="utf-8")
|
|
383
|
+
assert utils_module._read_original_account(list_payload_file) is None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def test_read_original_account_returns_none_for_non_string_value(
|
|
387
|
+
isolated_temp_directory: pathlib.Path,
|
|
388
|
+
) -> None:
|
|
389
|
+
bad_type_file = isolated_temp_directory / "bad_type.json"
|
|
390
|
+
bad_type_file.write_text(json.dumps({"original_account": 42}), encoding="utf-8")
|
|
391
|
+
assert utils_module._read_original_account(bad_type_file) is None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_read_original_account_returns_none_for_blank_value(
|
|
395
|
+
isolated_temp_directory: pathlib.Path,
|
|
396
|
+
) -> None:
|
|
397
|
+
blank_value_file = isolated_temp_directory / "blank.json"
|
|
398
|
+
blank_value_file.write_text(json.dumps({"original_account": " "}), encoding="utf-8")
|
|
399
|
+
assert utils_module._read_original_account(blank_value_file) is None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_read_original_account_returns_none_for_malformed_json(
|
|
403
|
+
isolated_temp_directory: pathlib.Path,
|
|
404
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
405
|
+
) -> None:
|
|
406
|
+
captured_stderr = io.StringIO()
|
|
407
|
+
monkeypatch.setattr(sys, "stderr", captured_stderr)
|
|
408
|
+
malformed_file = isolated_temp_directory / "malformed.json"
|
|
409
|
+
malformed_file.write_text("{not valid json", encoding="utf-8")
|
|
410
|
+
assert utils_module._read_original_account(malformed_file) is None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_delete_state_file_is_silent_when_already_absent(
|
|
414
|
+
isolated_temp_directory: pathlib.Path,
|
|
415
|
+
) -> None:
|
|
416
|
+
missing_file = isolated_temp_directory / "does_not_exist.json"
|
|
417
|
+
utils_module._delete_state_file(missing_file)
|
|
418
|
+
assert not missing_file.exists()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_delete_state_file_removes_existing_file(
|
|
422
|
+
isolated_temp_directory: pathlib.Path,
|
|
423
|
+
) -> None:
|
|
424
|
+
existing_file = isolated_temp_directory / "to_remove.json"
|
|
425
|
+
existing_file.write_text("payload", encoding="utf-8")
|
|
426
|
+
assert existing_file.exists()
|
|
427
|
+
utils_module._delete_state_file(existing_file)
|
|
428
|
+
assert not existing_file.exists()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_write_line_appends_newline_and_flushes() -> None:
|
|
432
|
+
captured_stream = io.StringIO()
|
|
433
|
+
utils_module._write_line("hello", captured_stream)
|
|
434
|
+
assert captured_stream.getvalue() == "hello\n"
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def test_write_line_writes_multiple_lines_in_call_order() -> None:
|
|
438
|
+
captured_stream = io.StringIO()
|
|
439
|
+
utils_module._write_line("first", captured_stream)
|
|
440
|
+
utils_module._write_line("second", captured_stream)
|
|
441
|
+
assert captured_stream.getvalue() == "first\nsecond\n"
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def test_all_gh_pr_create_segments_returns_empty_when_command_absent() -> None:
|
|
445
|
+
"""No ``gh pr create`` invocation → empty list."""
|
|
446
|
+
assert utils_module._all_gh_pr_create_segments("git status && echo done") == []
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def test_all_gh_pr_create_segments_returns_one_segment_for_single_invocation() -> None:
|
|
450
|
+
"""One invocation → one segment from end-of-match to end-of-string."""
|
|
451
|
+
segments_for_single_invocation = utils_module._all_gh_pr_create_segments(
|
|
452
|
+
"gh pr create --title T --body-file B"
|
|
453
|
+
)
|
|
454
|
+
assert len(segments_for_single_invocation) == 1
|
|
455
|
+
assert "--title T" in segments_for_single_invocation[0]
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def test_all_gh_pr_create_segments_returns_two_segments_for_chained_invocations() -> None:
|
|
459
|
+
"""Two chained invocations → two separate segments split at ``&&``."""
|
|
460
|
+
segments_for_chained_invocations = utils_module._all_gh_pr_create_segments(
|
|
461
|
+
"gh pr create --web && gh pr create --title T"
|
|
462
|
+
)
|
|
463
|
+
assert len(segments_for_chained_invocations) == 2
|
|
464
|
+
assert "--web" in segments_for_chained_invocations[0]
|
|
465
|
+
assert "--web" not in segments_for_chained_invocations[1]
|
|
466
|
+
assert "--title T" in segments_for_chained_invocations[1]
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def test_all_gh_pr_create_segments_splits_on_newline_separator() -> None:
|
|
470
|
+
"""Newline counts as a command separator between two ``gh pr create`` invocations."""
|
|
471
|
+
segments_for_newline_chained = utils_module._all_gh_pr_create_segments(
|
|
472
|
+
"gh pr create --web\ngh pr create --title T"
|
|
473
|
+
)
|
|
474
|
+
assert len(segments_for_newline_chained) == 2
|
|
475
|
+
assert "--web" in segments_for_newline_chained[0]
|
|
476
|
+
assert "--title T" in segments_for_newline_chained[1]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def test_strip_quoted_regions_blanks_single_quoted_argument_inside_substitution() -> None:
|
|
480
|
+
"""Regression for finding 2: ``$(printf 'gh pr create')`` must not leak the literal command.
|
|
481
|
+
|
|
482
|
+
The substitution executes ``printf`` against the literal data
|
|
483
|
+
``gh pr create`` — the data must not be confused with a real
|
|
484
|
+
``gh pr create`` invocation. Quoted regions inside substitution
|
|
485
|
+
bodies are blanked the same way as top-level quoted regions, so
|
|
486
|
+
the matcher sees ``$(printf )`` after stripping.
|
|
487
|
+
"""
|
|
488
|
+
original_command = "echo $(printf 'gh pr create')"
|
|
489
|
+
stripped_command = utils_module._strip_quoted_regions(original_command)
|
|
490
|
+
assert len(stripped_command) == len(original_command)
|
|
491
|
+
assert "gh pr create" not in stripped_command
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def test_command_invokes_gh_pr_create_rejects_data_argument_inside_substitution() -> None:
|
|
495
|
+
"""Regression for finding 2: ``echo $(printf 'gh pr create')`` runs printf, not gh pr create."""
|
|
496
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
497
|
+
utils_module._strip_quoted_regions("echo $(printf 'gh pr create')")
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def test_command_invokes_gh_pr_create_rejects_double_quoted_substitution_data() -> None:
|
|
502
|
+
"""Regression for finding 2: ``$(printf "gh pr create")`` runs printf, not gh pr create."""
|
|
503
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
504
|
+
utils_module._strip_quoted_regions('echo $(printf "gh pr create")')
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def test_command_invokes_gh_pr_create_still_detects_real_invocation_inside_substitution() -> None:
|
|
509
|
+
"""Regression for finding 2 guard: ``$(gh pr create)`` runs the real command — must still match."""
|
|
510
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
511
|
+
utils_module._strip_quoted_regions("echo $(gh pr create --title T)")
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_command_invokes_gh_pr_create_rejects_echo_argument() -> None:
|
|
516
|
+
"""Regression for finding 3: ``echo gh pr create`` is data passed to echo, not a command."""
|
|
517
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
518
|
+
utils_module._strip_quoted_regions("echo gh pr create")
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def test_command_invokes_gh_pr_create_rejects_inline_bash_comment() -> None:
|
|
523
|
+
"""Regression for finding 3: ``git status # gh pr create later`` is a comment, not a command."""
|
|
524
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
525
|
+
utils_module._strip_bash_comments(
|
|
526
|
+
utils_module._strip_quoted_regions("git status # gh pr create later")
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_command_invokes_gh_pr_create_rejects_standalone_bash_comment() -> None:
|
|
532
|
+
"""A line that begins with ``#`` is entirely comment — no command, no match."""
|
|
533
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
534
|
+
utils_module._strip_bash_comments(
|
|
535
|
+
utils_module._strip_quoted_regions("# gh pr create later")
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def test_command_invokes_gh_pr_create_still_matches_after_comment_on_prior_line() -> None:
|
|
541
|
+
"""A comment on a prior line is stripped; the real ``gh pr create`` on the next line still matches."""
|
|
542
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
543
|
+
utils_module._strip_bash_comments(
|
|
544
|
+
utils_module._strip_quoted_regions("# leave it commented\ngh pr create --title T")
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def test_preprocess_command_for_matching_chains_strip_and_comments() -> None:
|
|
550
|
+
"""The combined preprocess pipeline blanks quotes then comments in one step."""
|
|
551
|
+
preprocessed_command = utils_module._preprocess_command_for_matching(
|
|
552
|
+
'git status # gh pr create later --body "see docs"'
|
|
553
|
+
)
|
|
554
|
+
assert "see docs" not in preprocessed_command
|
|
555
|
+
assert "gh pr create" not in preprocessed_command
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def test_strip_substitution_bodies_replaces_dollar_paren_body_with_spaces() -> None:
|
|
559
|
+
"""Regression for finding 4: ``$(echo --web)`` body is blanked so ``--web`` no longer leaks."""
|
|
560
|
+
quote_stripped_command = utils_module._strip_quoted_regions(
|
|
561
|
+
'gh pr create --title "$(echo --web)" --body-file B'
|
|
562
|
+
)
|
|
563
|
+
bodies_blanked_command = utils_module._strip_substitution_bodies(quote_stripped_command)
|
|
564
|
+
assert len(bodies_blanked_command) == len(quote_stripped_command)
|
|
565
|
+
assert "--web" not in bodies_blanked_command
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def test_strip_substitution_bodies_replaces_backtick_body_with_spaces() -> None:
|
|
569
|
+
"""Regression for finding 4: backtick body is blanked so ``--web`` inside does not leak."""
|
|
570
|
+
quote_stripped_command = utils_module._strip_quoted_regions(
|
|
571
|
+
"gh pr create --title `echo --web` --body-file B"
|
|
572
|
+
)
|
|
573
|
+
bodies_blanked_command = utils_module._strip_substitution_bodies(quote_stripped_command)
|
|
574
|
+
assert len(bodies_blanked_command) == len(quote_stripped_command)
|
|
575
|
+
assert "--web" not in bodies_blanked_command
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def test_switch_gh_account_returns_false_on_permission_error() -> None:
|
|
579
|
+
"""Regression for finding 5: ``PermissionError`` from subprocess.run must be caught as failure."""
|
|
580
|
+
with mock.patch.object(utils_module.subprocess, "run", side_effect=PermissionError):
|
|
581
|
+
assert utils_module._switch_gh_account("JonEcho") is False
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def test_switch_gh_account_returns_false_on_generic_os_error() -> None:
|
|
585
|
+
"""Any ``OSError`` subclass from subprocess.run must follow the documented failure path."""
|
|
586
|
+
with mock.patch.object(utils_module.subprocess, "run", side_effect=OSError("spawn refused")):
|
|
587
|
+
assert utils_module._switch_gh_account("JonEcho") is False
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def test_command_invokes_gh_pr_create_matches_paren_subshell_prefix() -> None:
|
|
591
|
+
"""``( gh pr create --title T )`` is a real paren subshell — must match.
|
|
592
|
+
|
|
593
|
+
Bash executes commands inside ``( ... )`` in a subshell. The
|
|
594
|
+
boundary class in ``GH_PR_CREATE_PATTERN`` includes ``(`` so the
|
|
595
|
+
enforcer recognises the invocation.
|
|
596
|
+
"""
|
|
597
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
598
|
+
utils_module._preprocess_command_for_matching("( gh pr create --title T )")
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def test_command_invokes_gh_pr_create_matches_brace_group_prefix() -> None:
|
|
603
|
+
"""``{ gh pr create --title T ; }`` is a real brace group — must match.
|
|
604
|
+
|
|
605
|
+
Bash executes commands inside ``{ ...; }`` in the current shell.
|
|
606
|
+
The boundary class in ``GH_PR_CREATE_PATTERN`` includes ``{`` so
|
|
607
|
+
the enforcer recognises the invocation.
|
|
608
|
+
"""
|
|
609
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
610
|
+
utils_module._preprocess_command_for_matching("{ gh pr create --title T ; }")
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def test_command_invokes_gh_pr_create_matches_single_env_var_prefix() -> None:
|
|
615
|
+
"""``GH_DEBUG=1 gh pr create --title T`` is a real invocation with an env var assignment.
|
|
616
|
+
|
|
617
|
+
Bash applies the assignment to the ``gh`` process environment. The
|
|
618
|
+
pattern allows zero or more ``VAR=VALUE`` prefix segments before
|
|
619
|
+
the ``gh`` command name.
|
|
620
|
+
"""
|
|
621
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
622
|
+
utils_module._preprocess_command_for_matching("GH_DEBUG=1 gh pr create --title T")
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def test_command_invokes_gh_pr_create_matches_multiple_env_var_prefixes() -> None:
|
|
627
|
+
"""Multiple env var assignments stacked before ``gh`` must all be tolerated."""
|
|
628
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
629
|
+
utils_module._preprocess_command_for_matching(
|
|
630
|
+
"GH_DEBUG=1 GH_HOST=github.com gh pr create --title T"
|
|
631
|
+
)
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def test_command_invokes_gh_pr_create_rejects_shell_variable_expansion_prefix() -> None:
|
|
636
|
+
"""``${var} gh pr create`` is a shell variable expansion, not an env var assignment.
|
|
637
|
+
|
|
638
|
+
The env-var-assignment branch of the pattern requires a literal
|
|
639
|
+
``=`` character in the prefix segment. ``${var}`` carries no ``=``,
|
|
640
|
+
so the pattern correctly rejects it and the matcher returns False.
|
|
641
|
+
"""
|
|
642
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
643
|
+
utils_module._preprocess_command_for_matching("${var} gh pr create")
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def test_strip_bash_comments_strips_comment_inside_dollar_paren_substitution_body() -> None:
|
|
648
|
+
"""A ``#`` after whitespace inside ``$(...)`` is a comment INSIDE the substitution.
|
|
649
|
+
|
|
650
|
+
The substitution body executes as its own command, so the comment
|
|
651
|
+
must consume the trailing text inside the body — but ONLY up to
|
|
652
|
+
the closing ``)``. The ``echo $(echo ok # ; gh pr create)`` case
|
|
653
|
+
runs ``echo ok`` in the subshell; ``gh pr create`` is comment text
|
|
654
|
+
and must not match.
|
|
655
|
+
"""
|
|
656
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
657
|
+
utils_module._preprocess_command_for_matching("echo $(echo ok # ; gh pr create)")
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def test_strip_bash_comments_strips_comment_inside_backtick_substitution_body() -> None:
|
|
662
|
+
"""A ``#`` after whitespace inside ``` `...` ``` is a comment INSIDE the substitution.
|
|
663
|
+
|
|
664
|
+
Symmetric with ``$(...)`` — the backtick body executes, so a hash
|
|
665
|
+
after whitespace introduces a comment bounded by the closing
|
|
666
|
+
backtick.
|
|
667
|
+
"""
|
|
668
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
669
|
+
utils_module._preprocess_command_for_matching("echo `echo ok # gh pr create`")
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def test_strip_bash_comments_substitution_comment_does_not_consume_closer() -> None:
|
|
674
|
+
"""A comment inside a substitution body must terminate at the closer.
|
|
675
|
+
|
|
676
|
+
Without the closer-bound, a flat regex sweep would consume the
|
|
677
|
+
``)`` and every byte after it on the same line, erasing a real
|
|
678
|
+
``gh pr create`` that follows the substitution. The walker bounds
|
|
679
|
+
the comment at the closer so the trailing command stays visible.
|
|
680
|
+
"""
|
|
681
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
682
|
+
utils_module._preprocess_command_for_matching(
|
|
683
|
+
"$(date +%H # 24h) && gh pr create --title T"
|
|
684
|
+
)
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def test_strip_bash_comments_substitution_comment_in_backtick_does_not_consume_closer() -> None:
|
|
689
|
+
"""Backtick variant of the closer-bound: the trailing ``gh pr create`` stays visible."""
|
|
690
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
691
|
+
utils_module._preprocess_command_for_matching(
|
|
692
|
+
"foo `cmd # comment` bar && gh pr create"
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def test_strip_bash_comments_real_invocation_inside_substitution_still_matches() -> None:
|
|
698
|
+
"""A real ``$(gh pr create)`` (no comment) must still trigger the matcher."""
|
|
699
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
700
|
+
utils_module._preprocess_command_for_matching("echo $(gh pr create)")
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def test_strip_bash_comments_real_invocation_after_substitution_still_matches() -> None:
|
|
705
|
+
"""``echo $(echo ok); gh pr create`` — the trailing command is OUTSIDE the substitution."""
|
|
706
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
707
|
+
utils_module._preprocess_command_for_matching("echo $(echo ok); gh pr create")
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def test_strip_bash_comments_preserves_real_gh_pr_create_after_subshell_in_substitution() -> None:
|
|
712
|
+
"""An inner ``(subshell)`` inside ``$(...)`` must not let the walker exit early.
|
|
713
|
+
|
|
714
|
+
Without paren-depth tracking, the bare ``)`` of ``(subshell)`` would
|
|
715
|
+
match the outer substitution closer, leaving the walker at
|
|
716
|
+
``# comment) && gh pr create``. The walker would then treat ``#`` as
|
|
717
|
+
a top-level comment introducer and blank everything through the real
|
|
718
|
+
``)`` and the trailing ``gh pr create``, silently bypassing the
|
|
719
|
+
enforcer. Depth tracking keeps the outer substitution intact so the
|
|
720
|
+
real ``gh pr create`` after ``&&`` stays visible.
|
|
721
|
+
"""
|
|
722
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
723
|
+
utils_module._preprocess_command_for_matching(
|
|
724
|
+
"$(cmd; (subshell) # comment) && gh pr create"
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def test_strip_bash_comments_handles_deeply_nested_bare_parens_inside_substitution() -> None:
|
|
730
|
+
"""Multiple inner bare-paren groups inside ``$(...)`` resolve to their own closers.
|
|
731
|
+
|
|
732
|
+
``$(( 1 + 1 ))`` is bash arithmetic expansion that lexically
|
|
733
|
+
contains two opening parens and two closing parens, and
|
|
734
|
+
``(other_subshell)`` adds one more inner pair. Paren-depth tracking
|
|
735
|
+
ensures every inner pair cancels out before the walker accepts the
|
|
736
|
+
real outer ``)``, so the trailing ``gh pr create`` is reached.
|
|
737
|
+
"""
|
|
738
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
739
|
+
utils_module._preprocess_command_for_matching(
|
|
740
|
+
"echo $(echo $(( 1 + 1 )) (other_subshell) # x) && gh pr create"
|
|
741
|
+
)
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def test_strip_bash_comments_unterminated_substitution_with_inner_subshell_does_not_crash() -> None:
|
|
746
|
+
"""An unterminated ``$(...)`` body containing an inner ``(`` must not raise.
|
|
747
|
+
|
|
748
|
+
The walker increments depth on the inner ``(``, never finds the
|
|
749
|
+
matching outer ``)``, and reaches the end of the buffer. It must
|
|
750
|
+
return ``end_index`` gracefully rather than raising IndexError or
|
|
751
|
+
recursing forever.
|
|
752
|
+
"""
|
|
753
|
+
preprocessed_command = utils_module._preprocess_command_for_matching(
|
|
754
|
+
"$(echo (subshell"
|
|
755
|
+
)
|
|
756
|
+
assert isinstance(preprocessed_command, str)
|
|
757
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
758
|
+
preprocessed_command
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def test_strip_bash_comments_backtick_bound_ignores_bare_parens() -> None:
|
|
763
|
+
"""Backtick bodies do not track paren depth — bare parens inside are inert.
|
|
764
|
+
|
|
765
|
+
Backticks cannot nest in unescaped form, so paren depth tracking is
|
|
766
|
+
unnecessary. The walker treats a bare ``)`` inside ``` `...` ``` as
|
|
767
|
+
an ordinary character and exits the body only on the matching
|
|
768
|
+
closing backtick, leaving any trailing ``gh pr create`` visible.
|
|
769
|
+
"""
|
|
770
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
771
|
+
utils_module._preprocess_command_for_matching(
|
|
772
|
+
"`( inner ) # comment` && gh pr create"
|
|
773
|
+
)
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def test_strip_heredoc_bodies_blanks_single_quoted_tag_body() -> None:
|
|
778
|
+
"""Regression for finding 3: ``cat <<'EOF'\\ngh pr create\\nEOF`` body must not match.
|
|
779
|
+
|
|
780
|
+
The body of a single-quoted-tag heredoc is literal data fed to
|
|
781
|
+
``cat``. The matcher must blank the body so ``gh pr create`` inside
|
|
782
|
+
it does not trigger the enforcer.
|
|
783
|
+
"""
|
|
784
|
+
heredoc_command = "cat <<'EOF'\ngh pr create\nEOF"
|
|
785
|
+
blanked_command = utils_module._strip_heredoc_bodies(heredoc_command)
|
|
786
|
+
assert len(blanked_command) == len(heredoc_command)
|
|
787
|
+
assert "gh pr create" not in blanked_command
|
|
788
|
+
assert "EOF" in blanked_command
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def test_strip_heredoc_bodies_blanks_double_quoted_tag_body() -> None:
|
|
792
|
+
"""A double-quoted heredoc tag (``<<"EOF"``) is matched the same way as single-quoted."""
|
|
793
|
+
heredoc_command = "cat <<\"EOF\"\ngh pr create\nEOF"
|
|
794
|
+
blanked_command = utils_module._strip_heredoc_bodies(heredoc_command)
|
|
795
|
+
assert len(blanked_command) == len(heredoc_command)
|
|
796
|
+
assert "gh pr create" not in blanked_command
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def test_strip_heredoc_bodies_blanks_bare_tag_body() -> None:
|
|
800
|
+
"""A bare-tag heredoc (``<<EOF``) is detected the same as a quoted-tag form."""
|
|
801
|
+
heredoc_command = "cat <<EOF\ngh pr create\nEOF"
|
|
802
|
+
blanked_command = utils_module._strip_heredoc_bodies(heredoc_command)
|
|
803
|
+
assert len(blanked_command) == len(heredoc_command)
|
|
804
|
+
assert "gh pr create" not in blanked_command
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def test_strip_heredoc_bodies_allows_leading_tabs_for_dash_form() -> None:
|
|
808
|
+
"""The ``<<-`` form strips leading TAB characters on the closing tag line."""
|
|
809
|
+
heredoc_command = "cat <<-EOF\n\tgh pr create\n\tEOF"
|
|
810
|
+
blanked_command = utils_module._strip_heredoc_bodies(heredoc_command)
|
|
811
|
+
assert len(blanked_command) == len(heredoc_command)
|
|
812
|
+
assert "gh pr create" not in blanked_command
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def test_strip_heredoc_bodies_handles_multiple_heredocs_in_one_command() -> None:
|
|
816
|
+
"""Two heredocs in one command must each have their body blanked independently."""
|
|
817
|
+
heredoc_command = (
|
|
818
|
+
"cat <<'EOF1'\ngh pr create one\nEOF1\ncat <<'EOF2'\ngh pr create two\nEOF2"
|
|
819
|
+
)
|
|
820
|
+
blanked_command = utils_module._strip_heredoc_bodies(heredoc_command)
|
|
821
|
+
assert len(blanked_command) == len(heredoc_command)
|
|
822
|
+
assert "gh pr create one" not in blanked_command
|
|
823
|
+
assert "gh pr create two" not in blanked_command
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def test_strip_heredoc_bodies_leaves_unrelated_command_unchanged() -> None:
|
|
827
|
+
"""A command without any heredoc opener must pass through untouched."""
|
|
828
|
+
unaffected_command = "gh pr create --title T"
|
|
829
|
+
assert utils_module._strip_heredoc_bodies(unaffected_command) == unaffected_command
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def test_strip_heredoc_bodies_does_nothing_when_closing_tag_missing() -> None:
|
|
833
|
+
"""An apparent heredoc opener without a matching closing tag leaves the buffer alone.
|
|
834
|
+
|
|
835
|
+
The conservative branch protects against false positives where a
|
|
836
|
+
quoted ``<<TAG`` inside an unusual context lacks a real closer; the
|
|
837
|
+
walker must not erase a real ``gh pr create`` that follows on the
|
|
838
|
+
expectation of a body that does not exist.
|
|
839
|
+
"""
|
|
840
|
+
pseudo_heredoc_command = "cat <<EOF\nno closing tag here\ngh pr create --title T"
|
|
841
|
+
blanked_command = utils_module._strip_heredoc_bodies(pseudo_heredoc_command)
|
|
842
|
+
assert blanked_command == pseudo_heredoc_command
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def test_strip_heredoc_bodies_skips_here_string_triple_less_than() -> None:
|
|
846
|
+
"""``<<<`` is a here-string, not a heredoc, and has no body to blank."""
|
|
847
|
+
here_string_command = "command <<< 'literal input' && gh pr create --title T"
|
|
848
|
+
blanked_command = utils_module._strip_heredoc_bodies(here_string_command)
|
|
849
|
+
assert blanked_command == here_string_command
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def test_strip_heredoc_bodies_skips_double_less_inside_double_quotes() -> None:
|
|
853
|
+
"""A literal ``<<EOF`` inside ``"..."`` is text, not a heredoc opener."""
|
|
854
|
+
quoted_literal_command = 'echo "use <<EOF in your script" && gh pr create --title T'
|
|
855
|
+
blanked_command = utils_module._strip_heredoc_bodies(quoted_literal_command)
|
|
856
|
+
assert blanked_command == quoted_literal_command
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def test_strip_heredoc_bodies_skips_double_less_inside_single_quotes() -> None:
|
|
860
|
+
"""A literal ``<<EOF`` inside ``'...'`` is text, not a heredoc opener."""
|
|
861
|
+
quoted_literal_command = "echo 'use <<EOF docs' && gh pr create --title T"
|
|
862
|
+
blanked_command = utils_module._strip_heredoc_bodies(quoted_literal_command)
|
|
863
|
+
assert blanked_command == quoted_literal_command
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def test_command_invokes_gh_pr_create_rejects_heredoc_body_data() -> None:
|
|
867
|
+
"""Regression for finding 3: heredoc body data must not trigger the enforcer.
|
|
868
|
+
|
|
869
|
+
``cat <<'EOF'\\ngh pr create\\nEOF`` runs ``cat`` against literal
|
|
870
|
+
data; the data is not a command bash will execute. The full
|
|
871
|
+
preprocess pipeline must blank the body so the matcher returns
|
|
872
|
+
False.
|
|
873
|
+
"""
|
|
874
|
+
assert not utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
875
|
+
utils_module._preprocess_command_for_matching(
|
|
876
|
+
"cat <<'EOF'\ngh pr create\nEOF"
|
|
877
|
+
)
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def test_command_invokes_gh_pr_create_still_matches_real_invocation_after_heredoc() -> None:
|
|
882
|
+
"""A real ``gh pr create`` following a heredoc must still be detected.
|
|
883
|
+
|
|
884
|
+
The heredoc body is blanked but the surrounding command structure
|
|
885
|
+
stays scannable, so a trailing real invocation after the heredoc
|
|
886
|
+
closer triggers the matcher.
|
|
887
|
+
"""
|
|
888
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
889
|
+
utils_module._preprocess_command_for_matching(
|
|
890
|
+
"cat <<'EOF'\nbody data line\nEOF\ngh pr create --title T"
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def test_command_invokes_gh_pr_create_still_matches_real_invocation_before_heredoc() -> None:
|
|
896
|
+
"""A real ``gh pr create`` preceding a heredoc must still be detected."""
|
|
897
|
+
assert utils_module._command_invokes_gh_pr_create_in_stripped(
|
|
898
|
+
utils_module._preprocess_command_for_matching(
|
|
899
|
+
"gh pr create --title T && cat <<'EOF'\nbody line\nEOF"
|
|
900
|
+
)
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def test_advance_past_single_quoted_region_unterminated_returns_buffer_length() -> None:
|
|
905
|
+
"""An unterminated single-quoted region must clamp the return to ``buffer_length``."""
|
|
906
|
+
all_scanned_characters = ["'", "a", "b"]
|
|
907
|
+
advance_index = utils_module._advance_past_single_quoted_region(
|
|
908
|
+
all_scanned_characters, 0, 3
|
|
909
|
+
)
|
|
910
|
+
assert advance_index == 3
|