claude-dev-env 1.38.0 → 1.39.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 +10 -36
- package/_shared/pr-loop/audit-reply-template.md +147 -0
- package/_shared/pr-loop/fix-protocol.md +25 -4
- package/_shared/pr-loop/gh-payloads.md +37 -50
- package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
- package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +189 -0
- package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
- package/_shared/pr-loop/state-schema.md +1 -1
- package/agents/clean-coder.md +2 -2
- package/bin/install.mjs +6 -7
- package/bin/install.test.mjs +8 -0
- package/commands/doc-gist.md +16 -0
- package/commands/plan.md +0 -2
- package/commands/review-plan.md +1 -1
- package/docs/CODE_RULES.md +122 -2
- package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
- package/hooks/blocking/code_rules_enforcer.py +1236 -161
- package/hooks/blocking/convergence_gate_blocker.py +130 -0
- package/hooks/blocking/destructive_command_blocker.py +74 -0
- package/hooks/blocking/gh_body_arg_blocker.py +30 -0
- package/hooks/blocking/md_to_html_blocker.py +119 -0
- package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
- package/hooks/blocking/test_code_rules_enforcer.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
- package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
- package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
- package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
- package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
- package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +158 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
- package/hooks/blocking/test_destructive_command_blocker.py +146 -0
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
- package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
- package/hooks/blocking/test_md_to_html_blocker.py +317 -0
- package/hooks/config/any_type_config.py +7 -0
- package/hooks/config/banned_identifiers_constants.py +11 -0
- package/hooks/config/blocking_check_limits.py +38 -0
- package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
- package/hooks/config/code_rules_enforcer_constants.py +53 -0
- package/hooks/config/convergence_branch_constants.py +9 -0
- package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
- package/hooks/config/html_companion_constants.py +20 -0
- package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
- package/hooks/config/test_banned_identifiers_constants.py +17 -0
- package/hooks/hooks.json +28 -20
- package/hooks/pyproject.toml +69 -0
- package/hooks/validators/mypy_integration.py +47 -1
- package/hooks/validators/run_all_validators.py +3 -3
- package/hooks/validators/test_mypy_integration.py +50 -1
- package/hooks/workflow/doc_gist_auto_publish.py +144 -0
- package/hooks/workflow/md_to_html_companion.py +365 -0
- package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
- package/hooks/workflow/test_md_to_html_companion.py +452 -0
- package/package.json +1 -1
- package/rules/gh-body-file.md +2 -0
- package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
- package/scripts/check.ps1 +106 -0
- package/scripts/config/timing.py +11 -0
- package/scripts/sweep_empty_dirs.py +138 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_sweep_empty_dirs.py +183 -0
- package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
- package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
- package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
- package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
- package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
- package/skills/bugteam/CONSTRAINTS.md +21 -22
- package/skills/bugteam/EXAMPLES.md +3 -3
- package/skills/bugteam/PROMPTS.md +227 -67
- package/skills/bugteam/SKILL.md +114 -455
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +112 -39
- package/skills/bugteam/reference/audit-contract.md +4 -22
- package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
- package/skills/bugteam/reference/design-rationale.md +2 -2
- package/skills/bugteam/reference/github-pr-reviews.md +50 -57
- package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
- package/skills/bugteam/reference/team-setup.md +106 -9
- package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
- package/skills/bugteam/scripts/README.md +60 -0
- package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
- package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
- package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
- package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
- package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
- package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
- package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
- package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
- package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
- package/skills/bugteam/test_skill_additions.py +1 -11
- package/skills/code/SKILL.md +176 -0
- package/skills/doc-gist/SKILL.md +99 -0
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
- package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
- package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
- package/skills/doc-gist/references/examples/05-design-system.html +629 -0
- package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
- package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
- package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
- package/skills/doc-gist/references/examples/11-status-report.html +528 -0
- package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
- package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
- package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
- package/skills/doc-gist/references/examples/README.md +5 -0
- package/skills/doc-gist/scripts/config/__init__.py +0 -0
- package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
- package/skills/doc-gist/scripts/gist_upload.py +177 -0
- package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
- package/skills/findbugs/SKILL.md +68 -2
- package/skills/monitor-open-prs/SKILL.md +13 -32
- package/skills/monitor-open-prs/test_skill_contract.py +0 -11
- package/skills/pr-consistency-audit/SKILL.md +112 -0
- package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
- package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
- package/skills/pr-converge/SKILL.md +227 -23
- package/skills/pr-converge/config/__init__.py +0 -0
- package/skills/pr-converge/config/constants.py +62 -0
- package/skills/pr-converge/reference/convergence-gates.md +138 -44
- package/skills/pr-converge/reference/examples.md +43 -11
- package/skills/pr-converge/reference/fix-protocol.md +6 -5
- package/skills/pr-converge/reference/ground-rules.md +5 -3
- package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
- package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
- package/skills/pr-converge/reference/per-tick.md +90 -31
- package/skills/pr-converge/reference/state-schema.md +22 -1
- package/skills/pr-converge/reference/stop-conditions.md +9 -7
- package/skills/pr-converge/scripts/README.md +34 -46
- package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
- package/skills/pr-converge/scripts/check_convergence.py +497 -0
- package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
- package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
- package/skills/qbug/SKILL.md +132 -27
- package/skills/session-log/SKILL.md +216 -114
- package/skills/session-tidy/SKILL.md +1 -1
- package/skills/skill-builder/SKILL.md +138 -56
- package/skills/skill-builder/references/delegation-map.md +72 -113
- package/skills/skill-builder/references/progressive-disclosure.md +122 -0
- package/skills/skill-builder/references/self-audit-checklist.md +92 -0
- package/skills/skill-builder/references/skill-types.md +228 -0
- package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
- package/skills/skill-builder/templates/gap-analysis.md +15 -8
- package/skills/skill-builder/workflows/improve-skill.md +86 -57
- package/skills/skill-builder/workflows/new-skill.md +80 -168
- package/skills/skill-builder/workflows/polish-skill.md +78 -54
- package/skills/structure-prompt/SKILL.md +50 -0
- package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
- package/skills/structure-prompt/reference/block-classification.md +27 -0
- package/skills/structure-prompt/reference/canonical-case.md +48 -0
- package/skills/structure-prompt/reference/citation-depth.md +70 -0
- package/skills/structure-prompt/reference/cleanup.md +33 -0
- package/skills/structure-prompt/reference/constraints.md +33 -0
- package/skills/structure-prompt/reference/directives.md +37 -0
- package/skills/structure-prompt/reference/examples.md +72 -0
- package/skills/structure-prompt/reference/instantiation.md +51 -0
- package/skills/structure-prompt/reference/output-contract.md +72 -0
- package/skills/structure-prompt/reference/per-category.md +23 -0
- package/skills/structure-prompt/reference/persona.md +38 -0
- package/skills/structure-prompt/reference/research.md +33 -0
- package/skills/structure-prompt/reference/structure.md +28 -0
- package/agents/code-standards-agent.md +0 -93
- package/agents/groq-coder.md +0 -113
- package/agents/plan-executor.md +0 -226
- package/agents/project-docs-analyzer.md +0 -53
- package/agents/project-structure-organizer-agent.md +0 -72
- package/agents/skill-to-agent-converter.md +0 -370
- package/agents/skill-writer-agent.md +0 -470
- package/agents/user-docs-writer.md +0 -67
- package/agents/workflow-visual-documenter.md +0 -82
- package/commands/readability-review.md +0 -20
- package/hooks/mypy.ini +0 -2
- package/hooks/notification/attention_needed_notify.py +0 -71
- package/hooks/notification/claude_notification_handler.py +0 -67
- package/hooks/notification/notification_utils.py +0 -267
- package/hooks/notification/subagent_complete_notify.py +0 -381
- package/hooks/notification/test_attention_needed_notify.py +0 -47
- package/hooks/notification/test_claude_notification_handler.py +0 -54
- package/hooks/notification/test_notification_utils.py +0 -91
- package/hooks/notification/test_subagent_complete_notify.py +0 -79
- package/scripts/config/groq_bugteam_config.py +0 -230
- package/scripts/config/test_groq_bugteam_config.py +0 -83
- package/scripts/config/test_spec_implementer_prompt.py +0 -32
- package/scripts/groq_bugteam.README.md +0 -131
- package/scripts/groq_bugteam.py +0 -647
- package/scripts/groq_bugteam_dotenv.py +0 -40
- package/scripts/groq_bugteam_spec.py +0 -226
- package/scripts/test_groq_bugteam.py +0 -529
- package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
- package/scripts/test_groq_bugteam_dotenv.py +0 -66
- package/scripts/test_groq_bugteam_spec.py +0 -338
- package/skills/bugteam/SKILL_EVALS.md +0 -309
- package/skills/dream/SKILL.md +0 -118
- package/skills/ingest/SKILL.md +0 -40
- package/skills/npm-creator/SKILL.md +0 -187
- package/skills/readability-review/SKILL.md +0 -127
- package/skills/resume-review/SKILL.md +0 -261
- package/skills/rule-audit/SKILL.md +0 -307
- package/skills/rule-creator/SKILL.md +0 -150
- package/skills/searching-obsidian-vault/SKILL.md +0 -131
- package/skills/skill-writer/REFERENCE.md +0 -284
- package/skills/skill-writer/SKILL.md +0 -222
- package/skills/tdd-team/SKILL.md +0 -128
|
@@ -30,6 +30,7 @@ assert _hook_spec.loader is not None
|
|
|
30
30
|
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
31
31
|
_hook_spec.loader.exec_module(_hook_module)
|
|
32
32
|
check_unused_module_level_imports = _hook_module.check_unused_module_level_imports
|
|
33
|
+
_reconstruct_post_edit_file_content = _hook_module._reconstruct_post_edit_file_content
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
|
|
@@ -297,6 +298,163 @@ def test_should_skip_when_noqa_is_bare() -> None:
|
|
|
297
298
|
assert issues == [], f"Bare noqa must suppress unused import, got: {issues}"
|
|
298
299
|
|
|
299
300
|
|
|
301
|
+
def test_should_not_flag_imports_referenced_only_in_full_file_content() -> None:
|
|
302
|
+
fragment = "from config.constants import NEW_NAME\n"
|
|
303
|
+
full_file = (
|
|
304
|
+
"from config.constants import NEW_NAME\n"
|
|
305
|
+
"\n"
|
|
306
|
+
"def existing_function() -> str:\n"
|
|
307
|
+
" return NEW_NAME\n"
|
|
308
|
+
)
|
|
309
|
+
issues = check_unused_module_level_imports(
|
|
310
|
+
fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
|
|
311
|
+
)
|
|
312
|
+
assert issues == [], (
|
|
313
|
+
f"When the post-edit file references the import, it must not flag, got: {issues}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_should_flag_imports_unused_in_full_file_content() -> None:
|
|
318
|
+
fragment = "from config.constants import TRULY_UNUSED\n"
|
|
319
|
+
full_file = (
|
|
320
|
+
"from config.constants import TRULY_UNUSED\n"
|
|
321
|
+
"\n"
|
|
322
|
+
"def existing_function() -> None:\n"
|
|
323
|
+
" return None\n"
|
|
324
|
+
)
|
|
325
|
+
issues = check_unused_module_level_imports(
|
|
326
|
+
fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
|
|
327
|
+
)
|
|
328
|
+
assert any("TRULY_UNUSED" in each_issue for each_issue in issues), (
|
|
329
|
+
f"Imports unused in the post-edit file must still flag, got: {issues}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_should_only_flag_imports_in_fragment_not_full_file() -> None:
|
|
334
|
+
fragment = "from config.constants import FRAGMENT_IMPORT\n"
|
|
335
|
+
full_file = (
|
|
336
|
+
"from config.other import PRE_EXISTING_UNUSED\n"
|
|
337
|
+
"from config.constants import FRAGMENT_IMPORT\n"
|
|
338
|
+
"\n"
|
|
339
|
+
"def existing_function() -> str:\n"
|
|
340
|
+
" return FRAGMENT_IMPORT\n"
|
|
341
|
+
)
|
|
342
|
+
issues = check_unused_module_level_imports(
|
|
343
|
+
fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
|
|
344
|
+
)
|
|
345
|
+
assert issues == [], (
|
|
346
|
+
"Pre-existing imports outside the fragment must not be flagged on Edit; "
|
|
347
|
+
f"got: {issues}"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_should_skip_when_full_file_declares_dunder_all() -> None:
|
|
352
|
+
fragment = "from config.constants import NEW_NAME\n"
|
|
353
|
+
full_file = (
|
|
354
|
+
"from config.constants import NEW_NAME\n"
|
|
355
|
+
"\n"
|
|
356
|
+
"__all__ = ['NEW_NAME']\n"
|
|
357
|
+
)
|
|
358
|
+
issues = check_unused_module_level_imports(
|
|
359
|
+
fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
|
|
360
|
+
)
|
|
361
|
+
assert issues == [], (
|
|
362
|
+
"__all__ in the post-edit file must skip the scan even when the fragment "
|
|
363
|
+
f"itself does not contain __all__, got: {issues}"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
|
|
368
|
+
fragment = "from config.constants import NEW_NAME\n"
|
|
369
|
+
full_file = (
|
|
370
|
+
"from typing import TYPE_CHECKING\n"
|
|
371
|
+
"from config.constants import NEW_NAME\n"
|
|
372
|
+
"\n"
|
|
373
|
+
"if TYPE_CHECKING:\n"
|
|
374
|
+
" from somewhere import OtherName\n"
|
|
375
|
+
"\n"
|
|
376
|
+
"def run() -> None:\n"
|
|
377
|
+
" return None\n"
|
|
378
|
+
)
|
|
379
|
+
issues = check_unused_module_level_imports(
|
|
380
|
+
fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
|
|
381
|
+
)
|
|
382
|
+
assert issues == [], (
|
|
383
|
+
"TYPE_CHECKING gate in the post-edit file must skip the scan, "
|
|
384
|
+
f"got: {issues}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_should_fall_back_to_content_when_full_file_content_is_none() -> None:
|
|
389
|
+
source = (
|
|
390
|
+
"from config.constants import VENV_DIRECTORY_NAME\n"
|
|
391
|
+
"\n"
|
|
392
|
+
"def run() -> None:\n"
|
|
393
|
+
" return None\n"
|
|
394
|
+
)
|
|
395
|
+
issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH, full_file_content=None)
|
|
396
|
+
assert any("VENV_DIRECTORY_NAME" in each_issue for each_issue in issues), (
|
|
397
|
+
f"Backward-compat: with full_file_content=None, behavior must match "
|
|
398
|
+
f"the existing single-argument scan, got: {issues}"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_should_fall_back_when_full_file_content_has_syntax_error() -> None:
|
|
403
|
+
fragment = "from config.constants import NEW_NAME\n"
|
|
404
|
+
full_file_with_syntax_error = "from config import (\n not python\n"
|
|
405
|
+
issues = check_unused_module_level_imports(
|
|
406
|
+
fragment, PRODUCTION_FILE_PATH, full_file_content=full_file_with_syntax_error,
|
|
407
|
+
)
|
|
408
|
+
assert issues == [], (
|
|
409
|
+
"When the reconstructed post-edit content cannot be parsed, return empty "
|
|
410
|
+
f"rather than raising, got: {issues}"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def test_reconstruct_post_edit_returns_replaced_content(tmp_path: pathlib.Path) -> None:
|
|
415
|
+
target_file = tmp_path / "module.py"
|
|
416
|
+
target_file.write_text("ALPHA = 1\nBETA = 2\n", encoding="utf-8")
|
|
417
|
+
post_edit = _reconstruct_post_edit_file_content(
|
|
418
|
+
str(target_file), "BETA = 2", "BETA = 22\nGAMMA = 3",
|
|
419
|
+
)
|
|
420
|
+
assert post_edit == "ALPHA = 1\nBETA = 22\nGAMMA = 3\n", (
|
|
421
|
+
f"Helper must return the file body with the first occurrence replaced, got: {post_edit!r}"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_reconstruct_post_edit_returns_none_when_file_missing(tmp_path: pathlib.Path) -> None:
|
|
426
|
+
missing_file = tmp_path / "does_not_exist.py"
|
|
427
|
+
post_edit = _reconstruct_post_edit_file_content(
|
|
428
|
+
str(missing_file), "any_old", "any_new",
|
|
429
|
+
)
|
|
430
|
+
assert post_edit is None, (
|
|
431
|
+
f"Missing file must yield None so the caller treats it as 'no full-file context', got: {post_edit!r}"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def test_reconstruct_post_edit_returns_none_when_old_string_absent(tmp_path: pathlib.Path) -> None:
|
|
436
|
+
target_file = tmp_path / "module.py"
|
|
437
|
+
target_file.write_text("ALPHA = 1\n", encoding="utf-8")
|
|
438
|
+
post_edit = _reconstruct_post_edit_file_content(
|
|
439
|
+
str(target_file), "ZETA = 9", "OMEGA = 0",
|
|
440
|
+
)
|
|
441
|
+
assert post_edit is None, (
|
|
442
|
+
f"Absent old_string means the Edit will not apply cleanly — return None, got: {post_edit!r}"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def test_reconstruct_post_edit_replaces_only_first_occurrence(tmp_path: pathlib.Path) -> None:
|
|
447
|
+
target_file = tmp_path / "module.py"
|
|
448
|
+
target_file.write_text("X = 1\nX = 1\n", encoding="utf-8")
|
|
449
|
+
post_edit = _reconstruct_post_edit_file_content(
|
|
450
|
+
str(target_file), "X = 1", "X = 2",
|
|
451
|
+
)
|
|
452
|
+
assert post_edit == "X = 2\nX = 1\n", (
|
|
453
|
+
"Edit replaces only the first occurrence; helper must mirror that, got: "
|
|
454
|
+
f"{post_edit!r}"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
300
458
|
def test_should_flag_import_when_only_shadowed_local_name_is_loaded() -> None:
|
|
301
459
|
source = (
|
|
302
460
|
"import json\n"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Unit tests for convergence-gate-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
|
+
import re
|
|
12
|
+
|
|
13
|
+
_GH_PR_READY_PATTERN = re.compile(r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)")
|
|
14
|
+
|
|
15
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
16
|
+
"convergence_gate_blocker",
|
|
17
|
+
_HOOK_DIR / "convergence_gate_blocker.py",
|
|
18
|
+
)
|
|
19
|
+
assert hook_spec is not None
|
|
20
|
+
assert hook_spec.loader is not None
|
|
21
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
22
|
+
hook_spec.loader.exec_module(hook_module)
|
|
23
|
+
_resolve_pr_number = hook_module._resolve_pr_number
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_matches_gh_pr_ready_with_number() -> None:
|
|
27
|
+
assert _GH_PR_READY_PATTERN.search("gh pr ready 418")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_matches_gh_pr_ready_without_number() -> None:
|
|
31
|
+
assert _GH_PR_READY_PATTERN.search("gh pr ready")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_matches_gh_pr_ready_with_flags() -> None:
|
|
35
|
+
assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_does_not_match_gh_pr_create() -> None:
|
|
39
|
+
assert not _GH_PR_READY_PATTERN.search("gh pr create --title T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_does_not_match_gh_pr_view() -> None:
|
|
43
|
+
assert not _GH_PR_READY_PATTERN.search("gh pr view 418")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_does_not_match_gh_issue_close() -> None:
|
|
47
|
+
assert not _GH_PR_READY_PATTERN.search("gh issue close 42")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_extracts_pr_number_from_command() -> None:
|
|
51
|
+
assert _resolve_pr_number("gh pr ready 418", None) == 418
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_extracts_pr_number_with_flags() -> None:
|
|
55
|
+
assert _resolve_pr_number("gh pr ready 99 --undo", None) == 99
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_returns_none_when_no_number_and_no_repo() -> None:
|
|
59
|
+
assert _resolve_pr_number("gh pr ready", "/nonexistent/path") is None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_matches_gh_pr_ready_in_compound_command() -> None:
|
|
63
|
+
assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo && gh pr create")
|
|
@@ -1008,3 +1008,149 @@ def test_git_reset_hard_asks_when_settings_file_is_invalid_json(tmp_path: Path)
|
|
|
1008
1008
|
|
|
1009
1009
|
response = json.loads(result.stdout)
|
|
1010
1010
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
# --- convergence branch exemption unit tests ---
|
|
1014
|
+
|
|
1015
|
+
import importlib.util
|
|
1016
|
+
|
|
1017
|
+
_HOOK_DIR = Path(__file__).parent
|
|
1018
|
+
_hook_spec = importlib.util.spec_from_file_location(
|
|
1019
|
+
"destructive_command_blocker",
|
|
1020
|
+
_HOOK_DIR / "destructive_command_blocker.py",
|
|
1021
|
+
)
|
|
1022
|
+
assert _hook_spec is not None
|
|
1023
|
+
assert _hook_spec.loader is not None
|
|
1024
|
+
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
1025
|
+
_hook_spec.loader.exec_module(_hook_module)
|
|
1026
|
+
_force_push_targets_convergence_branch = _hook_module._force_push_targets_convergence_branch
|
|
1027
|
+
_is_convergence_branch = _hook_module._is_convergence_branch
|
|
1028
|
+
_all_refspecs_are_convergence_branches = _hook_module._all_refspecs_are_convergence_branches
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def test_convergence_branch_claude_prefix_allowed() -> None:
|
|
1032
|
+
assert _force_push_targets_convergence_branch(
|
|
1033
|
+
"git push --force origin claude/fix-123"
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def test_convergence_branch_worktree_prefix_allowed() -> None:
|
|
1038
|
+
assert _force_push_targets_convergence_branch(
|
|
1039
|
+
"git push --force origin worktree-pr-converge-418"
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def test_convergence_branch_pr_converge_allowed() -> None:
|
|
1044
|
+
assert _force_push_targets_convergence_branch(
|
|
1045
|
+
"git push --force origin pr-423-converge"
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def test_convergence_branch_f_variant_allowed() -> None:
|
|
1050
|
+
assert _force_push_targets_convergence_branch(
|
|
1051
|
+
"git push -f origin claude/fix-123"
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def test_convergence_branch_main_blocked() -> None:
|
|
1056
|
+
assert not _force_push_targets_convergence_branch(
|
|
1057
|
+
"git push --force origin main"
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def test_convergence_branch_refspec_destination_checked() -> None:
|
|
1062
|
+
assert not _force_push_targets_convergence_branch(
|
|
1063
|
+
"git push --force origin claude/fix:main"
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def test_convergence_branch_multi_refspec_main_blocked() -> None:
|
|
1068
|
+
assert not _force_push_targets_convergence_branch(
|
|
1069
|
+
"git push --force origin claude/fix-123 main"
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def test_convergence_branch_multi_refspec_all_convergence() -> None:
|
|
1074
|
+
assert _force_push_targets_convergence_branch(
|
|
1075
|
+
"git push --force origin claude/fix-123 worktree-other"
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def test_convergence_branch_multi_refspec_mixed_blocked() -> None:
|
|
1080
|
+
assert not _force_push_targets_convergence_branch(
|
|
1081
|
+
"git push --force origin claude/fix-123 main worktree-other"
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def test_convergence_branch_compound_main_piggyback_blocked() -> None:
|
|
1086
|
+
assert not _force_push_targets_convergence_branch(
|
|
1087
|
+
"git push --force origin claude/foo && git push --force origin main"
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def test_is_convergence_branch_claude_prefix() -> None:
|
|
1092
|
+
assert _is_convergence_branch("claude/fix-123")
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def test_is_convergence_branch_worktree_prefix() -> None:
|
|
1096
|
+
assert _is_convergence_branch("worktree-pr-418")
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def test_is_convergence_branch_pr_converge() -> None:
|
|
1100
|
+
assert _is_convergence_branch("pr-423-converge")
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def test_is_convergence_branch_main_rejected() -> None:
|
|
1104
|
+
assert not _is_convergence_branch("main")
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def test_is_convergence_branch_pr_converge_no_end_anchor() -> None:
|
|
1108
|
+
assert not _is_convergence_branch("pr-423-converge-extra")
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def test_all_refspecs_empty_string_returns_false() -> None:
|
|
1112
|
+
assert not _all_refspecs_are_convergence_branches("")
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def test_all_refspecs_whitespace_only_returns_false() -> None:
|
|
1116
|
+
assert not _all_refspecs_are_convergence_branches(" ")
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def test_all_refspecs_flag_only_returns_false() -> None:
|
|
1120
|
+
assert not _all_refspecs_are_convergence_branches("--no-verify")
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def test_all_refspecs_multiple_flags_only_returns_false() -> None:
|
|
1124
|
+
assert not _all_refspecs_are_convergence_branches("--no-verify --force")
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def test_all_refspecs_flag_then_branch_checks_branch() -> None:
|
|
1128
|
+
assert not _all_refspecs_are_convergence_branches("--force main")
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def test_all_refspecs_convergence_branch_with_flags() -> None:
|
|
1132
|
+
assert _all_refspecs_are_convergence_branches("--force claude/fix-123")
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def test_force_push_convergence_with_no_verify_blocked() -> None:
|
|
1136
|
+
payload = _make_bash_payload(
|
|
1137
|
+
"git push --force origin --no-verify claude/fix-123"
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
result = _run_rm_hook(payload)
|
|
1141
|
+
|
|
1142
|
+
response = json.loads(result.stdout)
|
|
1143
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1144
|
+
assert "--no-verify" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def test_force_push_convergence_with_no_gpg_sign_blocked() -> None:
|
|
1148
|
+
payload = _make_bash_payload(
|
|
1149
|
+
"git push --force origin --no-gpg-sign claude/fix-123"
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
result = _run_rm_hook(payload)
|
|
1153
|
+
|
|
1154
|
+
response = json.loads(result.stdout)
|
|
1155
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1156
|
+
assert "--no-gpg-sign" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for --no-verify / --no-gpg-sign blocking in destructive_command_blocker.
|
|
2
|
+
|
|
3
|
+
git-workflow.md:30-33 marks these as NON-NEGOTIABLE to skip — they bypass
|
|
4
|
+
hook signing and verification. The blocker must ASK before allowing them.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SCRIPT_PATH = Path(__file__).parent / "destructive_command_blocker.py"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
|
|
18
|
+
child_environment = os.environ.copy()
|
|
19
|
+
return subprocess.run(
|
|
20
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
21
|
+
input=json.dumps(payload),
|
|
22
|
+
text=True,
|
|
23
|
+
capture_output=True,
|
|
24
|
+
check=False,
|
|
25
|
+
env=child_environment,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_bash_payload(command: str) -> dict:
|
|
30
|
+
return {"tool_name": "Bash", "tool_input": {"command": command}}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_asks_on_git_commit_no_verify() -> None:
|
|
34
|
+
payload = _make_bash_payload('git commit -m "wip" --no-verify')
|
|
35
|
+
result = _run_hook(payload)
|
|
36
|
+
response = json.loads(result.stdout)
|
|
37
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
38
|
+
f"Expected ask for git commit --no-verify, got: {response!r}"
|
|
39
|
+
)
|
|
40
|
+
assert "no-verify" in response["hookSpecificOutput"]["permissionDecisionReason"], (
|
|
41
|
+
f"Reason must mention --no-verify, got: {response!r}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_asks_on_git_push_no_verify() -> None:
|
|
46
|
+
payload = _make_bash_payload("git push --no-verify origin main")
|
|
47
|
+
result = _run_hook(payload)
|
|
48
|
+
response = json.loads(result.stdout)
|
|
49
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
50
|
+
f"Expected ask for git push --no-verify, got: {response!r}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_asks_on_git_no_gpg_sign() -> None:
|
|
55
|
+
payload = _make_bash_payload("git commit --no-gpg-sign -m wip")
|
|
56
|
+
result = _run_hook(payload)
|
|
57
|
+
response = json.loads(result.stdout)
|
|
58
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
59
|
+
f"Expected ask for git --no-gpg-sign, got: {response!r}"
|
|
60
|
+
)
|
|
61
|
+
assert (
|
|
62
|
+
"no-gpg-sign" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
63
|
+
), f"Reason must mention --no-gpg-sign, got: {response!r}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_asks_on_git_commit_with_no_gpg_sign_config() -> None:
|
|
67
|
+
payload = _make_bash_payload("git -c commit.gpgsign=false commit -m wip")
|
|
68
|
+
result = _run_hook(payload)
|
|
69
|
+
response = json.loads(result.stdout)
|
|
70
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
71
|
+
f"Expected ask for -c commit.gpgsign=false, got: {response!r}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_asks_on_quoted_gpgsign_config() -> None:
|
|
76
|
+
payload = _make_bash_payload("git -c 'commit.gpgsign=false' commit -m wip")
|
|
77
|
+
result = _run_hook(payload)
|
|
78
|
+
response = json.loads(result.stdout)
|
|
79
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
80
|
+
f"Expected ask for quoted -c commit.gpgsign=false, got: {response!r}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_asks_on_value_quoted_gpgsign_config() -> None:
|
|
85
|
+
payload = _make_bash_payload("git -c commit.gpgsign='false' commit -m wip")
|
|
86
|
+
result = _run_hook(payload)
|
|
87
|
+
response = json.loads(result.stdout)
|
|
88
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
89
|
+
f"Expected ask for value-quoted -c commit.gpgsign='false', got: {response!r}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_normal_git_commit_passes() -> None:
|
|
94
|
+
payload = _make_bash_payload('git commit -m "real commit"')
|
|
95
|
+
result = _run_hook(payload)
|
|
96
|
+
if not result.stdout.strip():
|
|
97
|
+
return
|
|
98
|
+
response = json.loads(result.stdout)
|
|
99
|
+
decision = response.get("hookSpecificOutput", {}).get("permissionDecision", "allow")
|
|
100
|
+
assert decision != "ask", (
|
|
101
|
+
f"Normal git commit must not be flagged as destructive, got: {response!r}"
|
|
102
|
+
)
|
|
@@ -17,6 +17,7 @@ assert hook_spec.loader is not None
|
|
|
17
17
|
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
18
18
|
hook_spec.loader.exec_module(hook_module)
|
|
19
19
|
_uses_body_string_arg = hook_module._uses_body_string_arg
|
|
20
|
+
_has_backtick = hook_module._has_backtick
|
|
20
21
|
|
|
21
22
|
from _gh_body_arg_utils import iter_significant_tokens
|
|
22
23
|
|
|
@@ -373,3 +374,47 @@ def test_all_body_flag_prefixes_used_for_equals_skip() -> None:
|
|
|
373
374
|
for each_prefix in all_body_flag_prefixes:
|
|
374
375
|
assert each_prefix in _all_equals_prefixes_for_skip
|
|
375
376
|
|
|
377
|
+
|
|
378
|
+
def test_has_backtick_with_plain_text_body() -> None:
|
|
379
|
+
assert not _has_backtick('gh issue comment 42 --body "bugbot run"')
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def test_has_backtick_with_markdown_body() -> None:
|
|
383
|
+
assert _has_backtick('gh pr create --title "T" --body "Fixes `foo`"')
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def test_has_backtick_with_short_b_plain_text() -> None:
|
|
387
|
+
assert not _has_backtick('gh pr comment 10 -b "LGTM"')
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_has_backtick_with_equals_form_plain_text() -> None:
|
|
391
|
+
assert not _has_backtick('gh pr create --title "T" --body="bugbot run"')
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_has_backtick_with_empty_body() -> None:
|
|
395
|
+
assert not _has_backtick('gh pr create --title "T" --body=""')
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_has_backtick_bash_continuation_stripped() -> None:
|
|
399
|
+
"""Bash backslash line continuations are stripped before checking."""
|
|
400
|
+
command = 'gh pr create \\\n --title "T" \\\n --body "bugbot run"\n'
|
|
401
|
+
assert not _has_backtick(command)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_has_backtick_powershell_continuation_stripped() -> None:
|
|
405
|
+
"""PowerShell backtick line continuations are stripped before checking."""
|
|
406
|
+
command = 'gh pr create `\n --title "T" `\n --body "bugbot run"\n'
|
|
407
|
+
assert not _has_backtick(command)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_has_backtick_content_backtick_at_line_end() -> None:
|
|
411
|
+
"""A backtick at end of line in body content must be detected (not mistaken for continuation)."""
|
|
412
|
+
command = 'gh pr create --title "T" --body "Thanks `\n@user"'
|
|
413
|
+
assert _has_backtick(command)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_has_backtick_multi_line_body() -> None:
|
|
417
|
+
"""Backtick on a non-first line of a multi-line body must be detected."""
|
|
418
|
+
command = 'gh pr create --title "T" --body "First line.\nSecond with `code`."'
|
|
419
|
+
assert _has_backtick(command)
|
|
420
|
+
|