claude-dev-env 1.40.0 → 1.41.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 (39) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +53 -3
  3. package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
  4. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +68 -3
  5. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
  6. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  7. package/hooks/_gh_pr_author_swap_utils.py +1211 -0
  8. package/hooks/blocking/gh_body_arg_blocker.py +9 -6
  9. package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
  10. package/hooks/blocking/gh_pr_author_restore.py +100 -0
  11. package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
  12. package/hooks/blocking/pr_description_enforcer.py +1 -3
  13. package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
  14. package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
  15. package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
  16. package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
  17. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
  18. package/hooks/config/gh_pr_author_swap_constants.py +76 -0
  19. package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
  20. package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
  21. package/hooks/config/pr_description_enforcer_constants.py +5 -0
  22. package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
  23. package/hooks/hooks.json +40 -0
  24. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
  25. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
  26. package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
  27. package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
  28. package/hooks/test__gh_pr_author_swap_utils.py +333 -0
  29. package/package.json +1 -1
  30. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  31. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  32. package/skills/bugteam/reference/audit-contract.md +22 -0
  33. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  34. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
  35. package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
  36. package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
  37. package/skills/pr-converge/SKILL.md +8 -2
  38. package/skills/pr-converge/config/constants.py +2 -1
  39. package/skills/pr-converge/reference/state-schema.md +36 -8
@@ -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