claude-dev-env 4.2.0 → 4.3.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 (78) hide show
  1. package/.agents/skills/_shared/pr-loop/scripts/durable_post_lint.py +242 -0
  2. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py +106 -0
  3. package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint.py +332 -0
  4. package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py +25 -0
  5. package/.agents/skills/issue-tracker/SKILL.md +6 -0
  6. package/.agents/skills/pull-request/SKILL.md +218 -0
  7. package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
  8. package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
  9. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
  10. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +61 -0
  11. package/.agents/skills/pull-request/scripts/pull_request.py +276 -0
  12. package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
  13. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +49 -0
  14. package/.agents/skills/pull-request/scripts/test_pull_request.py +398 -0
  15. package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
  16. package/.agents/skills/test-runner/SKILL.md +69 -0
  17. package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
  18. package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
  19. package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
  20. package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
  21. package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
  22. package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
  23. package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
  24. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  25. package/bin/ever-shipped-skills.mjs +2 -0
  26. package/bin/ever-shipped-skills.test.mjs +8 -0
  27. package/bin/install.mjs +9 -1
  28. package/bin/install.prune.test.mjs +90 -0
  29. package/bin/install.test.mjs +24 -7
  30. package/hooks/blocking/pii_prevention_blocker.py +32 -51
  31. package/hooks/blocking/test_bash_post_call_dispatcher.py +5 -28
  32. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
  33. package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
  34. package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
  35. package/hooks/gh_post_body_texts.py +195 -0
  36. package/hooks/hooks.json +0 -15
  37. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -1
  38. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
  39. package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
  40. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +0 -7
  41. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
  42. package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
  43. package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
  44. package/hooks/observability/test_failure_recorder.py +1 -1
  45. package/hooks/pyproject.toml +1 -1
  46. package/hooks/test_gh_post_body_texts.py +79 -0
  47. package/package.json +1 -1
  48. package/rules/durable-post-artifacts.md +31 -20
  49. package/rules/gh-cli-conventions.md +23 -13
  50. package/rules/git-workflow.md +21 -8
  51. package/scripts/policy_lint/adapter_configuration.py +53 -12
  52. package/scripts/repository_checks/config/constants.py +0 -5
  53. package/scripts/tests/test_adapter_configuration.py +54 -0
  54. package/hooks/_gh_pr_author_swap_utils.py +0 -1225
  55. package/hooks/blocking/conventional_pr_title_gate.py +0 -443
  56. package/hooks/blocking/gh_body_arg_blocker.py +0 -176
  57. package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
  58. package/hooks/blocking/gh_pr_author_restore.py +0 -105
  59. package/hooks/blocking/pr_description_writer_gate.py +0 -256
  60. package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
  61. package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
  62. package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
  63. package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
  64. package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
  65. package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
  66. package/hooks/blocking/test_preflight_check.py +0 -343
  67. package/hooks/blocking/test_test_preflight_check.py +0 -80
  68. package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
  69. package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
  70. package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
  71. package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
  72. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
  73. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
  74. package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
  75. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
  76. package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
  77. package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
  78. package/hooks/test__gh_pr_author_swap_utils.py +0 -333
@@ -1,441 +0,0 @@
1
- """Unit tests for gh-body-arg-blocker PreToolUse hook."""
2
-
3
- import importlib.util
4
- import pathlib
5
- import sys
6
-
7
- _HOOK_DIR = pathlib.Path(__file__).parent
8
- if str(_HOOK_DIR) not in sys.path:
9
- sys.path.insert(0, str(_HOOK_DIR))
10
-
11
- hook_spec = importlib.util.spec_from_file_location(
12
- "gh_body_arg_blocker",
13
- _HOOK_DIR / "gh_body_arg_blocker.py",
14
- )
15
- assert hook_spec is not None
16
- assert hook_spec.loader is not None
17
- hook_module = importlib.util.module_from_spec(hook_spec)
18
- hook_spec.loader.exec_module(hook_module)
19
- _uses_body_string_arg = hook_module._uses_body_string_arg
20
- _has_backtick = hook_module._has_backtick
21
-
22
- from blocking._gh_body_arg_utils import (
23
- _all_equals_prefixes_for_skip,
24
- _quoted_value_starts_split,
25
- all_body_flag_prefixes,
26
- count_extra_tokens_to_skip_for_split_quoted_value,
27
- iter_significant_tokens,
28
- )
29
-
30
-
31
- def test_blocks_issue_create_with_body_string() -> None:
32
- assert _uses_body_string_arg('gh issue create --title "T" --body "text"')
33
-
34
-
35
- def test_blocks_issue_edit_with_body_string() -> None:
36
- assert _uses_body_string_arg('gh issue edit 42 --body "updated text"')
37
-
38
-
39
- def test_blocks_issue_comment_with_body_string() -> None:
40
- assert _uses_body_string_arg('gh issue comment 42 --body "my comment"')
41
-
42
-
43
- def test_blocks_pr_create_with_body_string() -> None:
44
- assert _uses_body_string_arg('gh pr create --title "T" --body "desc"')
45
-
46
-
47
- def test_blocks_pr_edit_with_body_string() -> None:
48
- assert _uses_body_string_arg('gh pr edit 10 --body "new desc"')
49
-
50
-
51
- def test_blocks_pr_comment_with_body_string() -> None:
52
- assert _uses_body_string_arg('gh pr comment 10 --body "LGTM"')
53
-
54
-
55
- def test_blocks_pr_review_with_body_string() -> None:
56
- assert _uses_body_string_arg('gh pr review 10 --approve --body "looks good"')
57
-
58
-
59
- def test_blocks_short_b_flag() -> None:
60
- assert _uses_body_string_arg('gh pr create --title "T" -b "desc"')
61
-
62
-
63
- def test_blocks_pr_create_with_body_equals_syntax() -> None:
64
- assert _uses_body_string_arg('gh pr create --title "T" --body="text"')
65
-
66
-
67
- def test_blocks_pr_create_with_short_b_equals_syntax() -> None:
68
- assert _uses_body_string_arg('gh pr create --title "T" -b="text"')
69
-
70
-
71
- def test_blocks_multiline_bash_continuation_body_on_later_line() -> None:
72
- command = 'gh pr create \\\n --title "T" \\\n --body "text"\n'
73
- assert _uses_body_string_arg(command)
74
-
75
-
76
- def test_blocks_multiline_continuation_with_trailing_whitespace() -> None:
77
- """Continuation marker followed by trailing spaces must still join lines."""
78
- command = 'gh pr create \\ \n --title "T" \\ \n --body "text"\n'
79
- assert _uses_body_string_arg(command)
80
-
81
-
82
- def test_blocks_multiline_powershell_continuation_body_on_later_line() -> None:
83
- """PowerShell backtick continuation lines must be joined before tokenizing."""
84
- command = 'gh pr create `\n --title "T" `\n --body "text"\n'
85
- assert _uses_body_string_arg(command)
86
-
87
-
88
- def test_blocks_multiline_powershell_continuation_with_trailing_whitespace() -> None:
89
- """PowerShell backtick continuation with trailing spaces must still join."""
90
- command = 'gh pr create ` \n --title "T" ` \n --body "text"\n'
91
- assert _uses_body_string_arg(command)
92
-
93
-
94
- def test_approves_body_file() -> None:
95
- assert not _uses_body_string_arg(
96
- 'gh pr create --title "T" --body-file /tmp/body.md'
97
- )
98
-
99
-
100
- def test_approves_body_file_equals_syntax() -> None:
101
- assert not _uses_body_string_arg(
102
- 'gh pr create --title "T" --body-file=/tmp/body.md'
103
- )
104
-
105
-
106
- def test_approves_issue_create_with_body_file() -> None:
107
- assert not _uses_body_string_arg(
108
- 'gh issue create --title "T" --body-file /tmp/body.md'
109
- )
110
-
111
-
112
- def test_approves_unrelated_gh_command() -> None:
113
- assert not _uses_body_string_arg("gh pr list --repo owner/repo")
114
-
115
-
116
- def test_approves_gh_pr_merge() -> None:
117
- assert not _uses_body_string_arg("gh pr merge 10 --squash")
118
-
119
-
120
- def test_approves_empty_command() -> None:
121
- assert not _uses_body_string_arg("")
122
-
123
-
124
- def test_no_false_positive_body_in_title_value() -> None:
125
- """--body inside a quoted --title value must not trigger."""
126
- assert not _uses_body_string_arg(
127
- 'gh pr create --title "block gh --body string arg" --body-file /tmp/b.md'
128
- )
129
-
130
-
131
- def test_no_false_positive_body_as_title_value() -> None:
132
- """--title "--body" must not trigger; posix=False retains quotes so the value is not a bare flag."""
133
- assert not _uses_body_string_arg(
134
- 'gh pr create --title "--body" --body-file /tmp/b.md'
135
- )
136
-
137
-
138
- def test_no_false_positive_windows_path_in_body_file() -> None:
139
- """Unquoted Windows path with backslashes in --body-file must be approved without token corruption."""
140
- assert not _uses_body_string_arg(
141
- r'gh pr create --title "T" --body-file C:\Users\example\tmp\body.md'
142
- )
143
-
144
-
145
- def test_no_false_positive_heredoc_body_text() -> None:
146
- """Multiline command where body content mentions --body must not trigger."""
147
- command = (
148
- "gh pr create --title 'My PR' --body-file /tmp/body.md\n"
149
- "# body content below mentions --body for documentation\n"
150
- '# Use --body-file not --body "string"\n'
151
- )
152
- assert not _uses_body_string_arg(command)
153
-
154
-
155
- def test_no_false_positive_unparseable_command() -> None:
156
- """Unparseable line WITHOUT --body token must approve (out of hook scope)."""
157
- assert not _uses_body_string_arg("gh pr create --title 'unmatched quote here")
158
-
159
-
160
- def test_blocks_unparseable_command_when_body_token_present() -> None:
161
- """C2: shlex-unparseable command on affected subcommand WITH --body must BLOCK.
162
-
163
- A heredoc-style command like `gh pr create --body "$(cat <<EOF...)"` raises
164
- ValueError in shlex.split(posix=False); silently approving lets the exact
165
- pattern this hook exists to block slip through. Detect a bare --body literal
166
- via regex and deny.
167
- """
168
- command = 'gh pr create --title "T" --body "$(cat <<EOF\nbody text\nEOF\n)"'
169
- assert _uses_body_string_arg(command)
170
-
171
-
172
- def test_blocks_unparseable_command_with_short_b_token_present() -> None:
173
- """C2 short form: unparseable command (odd single quote) with bare -b must BLOCK."""
174
- command = "gh pr comment 42 --unterminated 'quote here -b short body text"
175
- assert _uses_body_string_arg(command)
176
-
177
-
178
- def test_approves_body_file_dash_stdin_sentinel() -> None:
179
- """`gh pr create --body-file -` reads body from stdin and is allowed."""
180
- assert not _uses_body_string_arg("gh pr create --title 'T' --body-file -")
181
-
182
-
183
- def test_blocks_crlf_line_endings_with_bash_continuation() -> None:
184
- """CRLF line endings must not break bash continuation joining."""
185
- command = 'gh pr create \\\r\n --title "T" \\\r\n --body "text"\r\n'
186
- assert _uses_body_string_arg(command)
187
-
188
-
189
- def test_blocks_continuation_with_tab_after_marker() -> None:
190
- """Tab whitespace after a continuation marker still counts as continuation."""
191
- command = 'gh pr create \\\t\n --title "T" \\\t\n --body "text"\n'
192
- assert _uses_body_string_arg(command)
193
-
194
-
195
- def test_blocks_utf8_body_with_emoji() -> None:
196
- """UTF-8 body content (emoji) must still be detected as a body string."""
197
- assert _uses_body_string_arg('gh pr create --title "T" --body "ship it 🚀"')
198
-
199
-
200
- def test_blocks_pr_review_request_changes_short_b() -> None:
201
- """gh pr review --request-changes -b "text" must block."""
202
- assert _uses_body_string_arg(
203
- 'gh pr review 10 --request-changes -b "needs work"'
204
- )
205
-
206
-
207
- def test_blocks_empty_body_string() -> None:
208
- """`gh pr create --body=""` is still a body-string call; must block."""
209
- assert _uses_body_string_arg('gh pr create --title "T" --body=""')
210
-
211
-
212
- def test_blocks_body_file_followed_by_body_string() -> None:
213
- """H5: malformed `gh pr create --body-file --body "text"` must BLOCK.
214
-
215
- Without the flag-shape guard, --body-file consumes --body as its path and
216
- the real body string slips through as a positional. Treat a value-taking
217
- flag as value-missing when the next token is itself flag-shaped.
218
- """
219
- assert _uses_body_string_arg(
220
- 'gh pr create --body-file --body "real body text"'
221
- )
222
-
223
-
224
- def test_blocks_windows_path_with_trailing_backslash_continuation() -> None:
225
- """C1: prior line ending in `C:\\Users\\example\\` must still continue lines.
226
-
227
- Naive `count("\\\\") % 2 == 1` mis-classifies this (count=4, even -> no
228
- continuation) and misses --body on the next line. Counting only the
229
- trailing run of backslashes correctly identifies a single trailing
230
- backslash as a continuation marker.
231
- """
232
- command = (
233
- 'gh pr create --title C:\\Users\\example\\ \\\n'
234
- ' --body "real body text"\n'
235
- )
236
- assert _uses_body_string_arg(command)
237
-
238
-
239
- def test_does_not_join_lines_after_markdown_fence() -> None:
240
- """C1: a prior line ending in three backticks (count=3, odd) must NOT continue.
241
-
242
- Old logic treated any odd backtick count as a PowerShell continuation; a
243
- closing markdown fence trailing the line would falsely join the next line.
244
- The fix requires whitespace before a trailing backtick (PowerShell
245
- continuation marker is `<space>` + backtick + newline) so a bare ``` line
246
- end does not continue.
247
- """
248
- command = (
249
- "echo ```\n"
250
- 'gh pr create --title "T" --body "real body text"\n'
251
- )
252
- assert _uses_body_string_arg(command) is False
253
-
254
-
255
- def test_does_not_false_positive_equals_form_in_title_value() -> None:
256
- """`--title="use --body x"` posix=False keeps the value quoted; must approve."""
257
- assert not _uses_body_string_arg(
258
- 'gh pr create --title="use --body x" --body-file /tmp/b.md'
259
- )
260
-
261
-
262
- def test_blocks_short_body_file_F_then_body_string_later() -> None:
263
- """`-F /path` (short for --body-file) consumes its value, then --body must still block."""
264
- assert _uses_body_string_arg(
265
- 'gh pr create -F /tmp/file.md --body "extra body"'
266
- )
267
-
268
-
269
- def test_approves_short_body_file_F_alone() -> None:
270
- """`-F /path` alone must be approved (it is the short form of --body-file)."""
271
- assert not _uses_body_string_arg(
272
- 'gh pr create --title "T" -F /tmp/file.md'
273
- )
274
-
275
-
276
- def test_approves_template_flag_value_does_not_trigger() -> None:
277
- """`--template path` consumes its value; `--body` inside path must not trigger."""
278
- assert not _uses_body_string_arg(
279
- 'gh pr create --title "T" --template /tmp/--body-template.md --body-file /tmp/b.md'
280
- )
281
-
282
-
283
- def test_approves_recover_flag_value() -> None:
284
- """`--recover path` is value-taking and must not leak `--body` mis-detection."""
285
- assert not _uses_body_string_arg(
286
- 'gh pr create --recover /tmp/state.json --body-file /tmp/b.md'
287
- )
288
-
289
-
290
- def test_approves_body_file_only_shlex_unparseable() -> None:
291
- """loop1-1: shlex-unparseable command with only --body-file must NOT block.
292
-
293
- The old \b boundary in _BARE_BODY_TOKEN_PATTERN fired between 'y' and '-'
294
- in '--body-file', causing any unparseable command containing only --body-file
295
- (no bare --body) to be incorrectly blocked.
296
- """
297
- assert not _uses_body_string_arg(
298
- "gh pr create --title 'unmatched --body-file /tmp/body.md"
299
- )
300
-
301
-
302
- def test_blocks_body_equals_spaced_value() -> None:
303
- """loop1-3: --body='has space' split by shlex(posix=False) into two tokens.
304
-
305
- shlex.split(posix=False) splits `--body='has space'` into ["--body='has", "space'"].
306
- The continuation token "space'" must be skipped, not yielded as a significant
307
- positional — the leading --body='has token must still trigger a block.
308
- """
309
- assert _uses_body_string_arg(
310
- "gh pr create --title 'T' --body='has space'"
311
- )
312
-
313
-
314
- def test_blocks_short_b_equals_spaced_value() -> None:
315
- """loop1-3: -b='has space' split by shlex(posix=False) — continuation token skipped."""
316
- assert _uses_body_string_arg(
317
- "gh pr create --title 'T' -b='has space'"
318
- )
319
-
320
-
321
- def test_blocks_body_after_unclosed_quoted_title() -> None:
322
- """loop2-1: unclosed quote in --title= must not consume --body token.
323
-
324
- shlex.split(posix=False) keeps --title="unclosed as one token whose value
325
- starts with a quote but has no closing match among subsequent tokens.
326
- count_extra_tokens_to_skip_for_split_quoted_value must return 0 (not the
327
- full remaining list length) so the following --body flag stays visible.
328
- """
329
- assert _uses_body_string_arg(
330
- 'gh pr create --title="unclosed --body "real body"'
331
- )
332
-
333
-
334
- def test_space_form_value_flag_remaining_excludes_consumed_value() -> None:
335
- """loop2-2: remaining_tokens for space-form value flag must not include the consumed value.
336
-
337
- When iter_significant_tokens yields (--title, remaining) for --title MyTitle,
338
- remaining must contain only tokens after MyTitle — not MyTitle itself.
339
- """
340
- all_yielded = list(iter_significant_tokens('gh pr create --title MyTitle --body "desc"'))
341
- title_remaining = next(remaining for token, remaining in all_yielded if token == "--title")
342
- assert "MyTitle" not in title_remaining
343
-
344
-
345
- def test_quoted_value_starts_split_unclosed_single_quote() -> None:
346
- assert _quoted_value_starts_split("'it") is True
347
-
348
-
349
- def test_quoted_value_starts_split_fully_closed() -> None:
350
- assert _quoted_value_starts_split("'hello'") is False
351
-
352
-
353
- def test_quoted_value_starts_split_double_quote_unclosed() -> None:
354
- assert _quoted_value_starts_split('"hello') is True
355
-
356
-
357
- def test_count_extra_tokens_returns_none_when_exhausted() -> None:
358
- result = count_extra_tokens_to_skip_for_split_quoted_value([], "'unclosed")
359
- assert result is None
360
-
361
-
362
- def test_count_extra_tokens_returns_none_no_closing_in_remaining() -> None:
363
- result = count_extra_tokens_to_skip_for_split_quoted_value(["word", "another"], "'unclosed")
364
- assert result is None
365
-
366
-
367
- def test_count_extra_tokens_returns_zero_for_self_contained() -> None:
368
- result = count_extra_tokens_to_skip_for_split_quoted_value(["next"], "'complete'")
369
- assert result == 0
370
-
371
-
372
- def test_all_body_flag_prefixes_used_for_equals_skip() -> None:
373
- for each_prefix in all_body_flag_prefixes:
374
- assert each_prefix in _all_equals_prefixes_for_skip
375
-
376
-
377
- def test_has_backtick_with_plain_text_body() -> None:
378
- assert not _has_backtick('gh issue comment 42 --body "bugbot run"')
379
-
380
-
381
- def test_has_backtick_with_markdown_body() -> None:
382
- assert _has_backtick('gh pr create --title "T" --body "Fixes `foo`"')
383
-
384
-
385
- def test_has_backtick_with_short_b_plain_text() -> None:
386
- assert not _has_backtick('gh pr comment 10 -b "LGTM"')
387
-
388
-
389
- def test_has_backtick_with_equals_form_plain_text() -> None:
390
- assert not _has_backtick('gh pr create --title "T" --body="bugbot run"')
391
-
392
-
393
- def test_has_backtick_with_empty_body() -> None:
394
- assert not _has_backtick('gh pr create --title "T" --body=""')
395
-
396
-
397
- def test_has_backtick_bash_continuation_stripped() -> None:
398
- """Bash backslash line continuations are stripped before checking."""
399
- command = 'gh pr create \\\n --title "T" \\\n --body "bugbot run"\n'
400
- assert not _has_backtick(command)
401
-
402
-
403
- def test_has_backtick_powershell_continuation_not_stripped_in_bash_context() -> None:
404
- """PowerShell backtick continuations are real backticks under the Bash tool.
405
-
406
- The hook runs on Bash tool invocations where PowerShell-style backtick
407
- continuations are not continuation markers. Any backtick character in
408
- the command is a literal backtick that must be detected.
409
- """
410
- command = 'gh pr create `\n --title "T" `\n --body "bugbot run"\n'
411
- assert _has_backtick(command)
412
-
413
-
414
- def test_has_backtick_content_backtick_at_line_end() -> None:
415
- """A backtick at end of line in body content must be detected (not mistaken for continuation)."""
416
- command = 'gh pr create --title "T" --body "Thanks `\n@user"'
417
- assert _has_backtick(command)
418
-
419
-
420
- def test_has_backtick_multi_line_body() -> None:
421
- """Backtick on a non-first line of a multi-line body must be detected."""
422
- command = 'gh pr create --title "T" --body "First line.\nSecond with `code`."'
423
- assert _has_backtick(command)
424
-
425
-
426
- def test_has_backtick_inside_unclosed_double_quoted_body_value() -> None:
427
- """Trailing backtick inside an unclosed --body "..." opening line counts as body content.
428
-
429
- The first line carries an opening double-quote for --body that is not
430
- closed on that line (odd quote count = 3). The trailing backtick is body
431
- content (an inline-code opening), not a PowerShell continuation marker.
432
- The unclosed-quote check preserves the original line including its
433
- trailing backtick, so _has_backtick sees the backtick and returns True.
434
- """
435
- command = (
436
- 'gh pr create --title "T" --body "Thanks `\n'
437
- ' this continues the body\n'
438
- ' with more text"\n'
439
- )
440
- assert _has_backtick(command)
441
-