claude-dev-env 1.38.1 → 1.40.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 +199 -0
- package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
- package/_shared/pr-loop/scripts/post_audit_thread.py +1242 -0
- package/_shared/pr-loop/scripts/preflight.py +129 -2
- package/_shared/pr-loop/scripts/reviews_disabled.py +59 -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 +1116 -0
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
- package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
- package/_shared/pr-loop/state-schema.md +1 -1
- package/agents/clean-coder.md +2 -2
- package/agents/pr-description-writer.md +150 -52
- 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/docs/PR_DESCRIPTION_GUIDE.md +127 -64
- package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
- package/hooks/blocking/code_rules_enforcer.py +1143 -129
- 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/pr_description_enforcer.py +57 -22
- 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_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/blocking/test_pr_description_enforcer.py +69 -8
- 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/pr_description_enforcer_constants.py +14 -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 +132 -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 +111 -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 +328 -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 +309 -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/copilot-review/SKILL.md +16 -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 +96 -2
- package/skills/monitor-open-prs/SKILL.md +14 -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 +229 -23
- package/skills/pr-converge/config/__init__.py +0 -0
- package/skills/pr-converge/config/constants.py +63 -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 +107 -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 +279 -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/scripts/test_check_bugbot_ci.py +312 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
- package/skills/qbug/SKILL.md +157 -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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Tests for check_typed_dict_encode_decode — flags TypedDicts missing companion encoders.
|
|
2
|
+
|
|
3
|
+
Per Plan 1c.typed_dict_validator / Phase B2: every TypedDict declaration in
|
|
4
|
+
production code must have a companion `_encode_<snake_name>` and
|
|
5
|
+
`_decode_<snake_name>` function so untyped dicts cannot leak across module
|
|
6
|
+
boundaries without explicit validation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_enforcer_module() -> ModuleType:
|
|
17
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
18
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
19
|
+
assert spec is not None
|
|
20
|
+
assert spec.loader is not None
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(module)
|
|
23
|
+
return module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_typed_dict_encode_decode(content: str, file_path: str) -> list[str]:
|
|
30
|
+
return code_rules_enforcer.check_typed_dict_encode_decode(content, file_path)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
PRODUCTION_FILE_PATH = "/project/src/contracts.py"
|
|
34
|
+
TEST_FILE_PATH = "/project/src/test_contracts.py"
|
|
35
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_should_flag_typed_dict_without_encode_or_decode() -> None:
|
|
39
|
+
source = (
|
|
40
|
+
"from typing import TypedDict\n"
|
|
41
|
+
"class InvoicePayload(TypedDict):\n"
|
|
42
|
+
" amount: int\n"
|
|
43
|
+
)
|
|
44
|
+
issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
|
|
45
|
+
assert any("InvoicePayload" in each for each in issues), (
|
|
46
|
+
f"Expected InvoicePayload to be flagged, got: {issues!r}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_should_flag_typed_dict_with_only_encode() -> None:
|
|
51
|
+
source = (
|
|
52
|
+
"from typing import TypedDict\n"
|
|
53
|
+
"class InvoicePayload(TypedDict):\n"
|
|
54
|
+
" amount: int\n"
|
|
55
|
+
"def _encode_invoice_payload(value: InvoicePayload) -> bytes:\n"
|
|
56
|
+
" return b''\n"
|
|
57
|
+
)
|
|
58
|
+
issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
|
|
59
|
+
assert any(
|
|
60
|
+
"InvoicePayload" in each and "decode" in each.lower() for each in issues
|
|
61
|
+
), f"Expected missing _decode_ to be flagged, got: {issues!r}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_should_flag_typed_dict_with_only_decode() -> None:
|
|
65
|
+
source = (
|
|
66
|
+
"from typing import TypedDict\n"
|
|
67
|
+
"class InvoicePayload(TypedDict):\n"
|
|
68
|
+
" amount: int\n"
|
|
69
|
+
"def _decode_invoice_payload(raw: bytes) -> InvoicePayload:\n"
|
|
70
|
+
" return {'amount': 0}\n"
|
|
71
|
+
)
|
|
72
|
+
issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
|
|
73
|
+
assert any(
|
|
74
|
+
"InvoicePayload" in each and "encode" in each.lower() for each in issues
|
|
75
|
+
), f"Expected missing _encode_ to be flagged, got: {issues!r}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_should_not_flag_typed_dict_with_both_companions() -> None:
|
|
79
|
+
source = (
|
|
80
|
+
"from typing import TypedDict\n"
|
|
81
|
+
"class InvoicePayload(TypedDict):\n"
|
|
82
|
+
" amount: int\n"
|
|
83
|
+
"def _encode_invoice_payload(value: InvoicePayload) -> bytes:\n"
|
|
84
|
+
" return b''\n"
|
|
85
|
+
"def _decode_invoice_payload(raw: bytes) -> InvoicePayload:\n"
|
|
86
|
+
" return {'amount': 0}\n"
|
|
87
|
+
)
|
|
88
|
+
issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
|
|
89
|
+
assert issues == [], f"Both companions present, got: {issues!r}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_should_handle_pascal_to_snake_conversion() -> None:
|
|
93
|
+
source = (
|
|
94
|
+
"from typing import TypedDict\n"
|
|
95
|
+
"class TypedAuthRequest(TypedDict):\n"
|
|
96
|
+
" token: str\n"
|
|
97
|
+
)
|
|
98
|
+
issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
|
|
99
|
+
assert any("TypedAuthRequest" in each for each in issues), (
|
|
100
|
+
f"PascalCase conversion expected; got: {issues!r}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_should_skip_test_file() -> None:
|
|
105
|
+
source = "from typing import TypedDict\nclass MockPayload(TypedDict):\n x: int\n"
|
|
106
|
+
issues = check_typed_dict_encode_decode(source, TEST_FILE_PATH)
|
|
107
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
111
|
+
source = "from typing import TypedDict\nclass HookPayload(TypedDict):\n x: int\n"
|
|
112
|
+
issues = check_typed_dict_encode_decode(source, HOOK_INFRASTRUCTURE_PATH)
|
|
113
|
+
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
117
|
+
source = "class InvoicePayload(TypedDict\n"
|
|
118
|
+
issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
|
|
119
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_should_not_flag_typed_dict_nested_inside_class() -> None:
|
|
123
|
+
source = (
|
|
124
|
+
"from typing import TypedDict\n"
|
|
125
|
+
"class Service:\n"
|
|
126
|
+
" class RequestPayload(TypedDict):\n"
|
|
127
|
+
" token: str\n"
|
|
128
|
+
)
|
|
129
|
+
issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
|
|
130
|
+
assert issues == [], f"Nested TypedDict must not be flagged, got: {issues!r}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_should_not_flag_non_typed_dict_class() -> None:
|
|
134
|
+
source = (
|
|
135
|
+
"from dataclasses import dataclass\n"
|
|
136
|
+
"@dataclass\n"
|
|
137
|
+
"class Invoice:\n"
|
|
138
|
+
" amount: int\n"
|
|
139
|
+
)
|
|
140
|
+
issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
|
|
141
|
+
assert issues == [], f"Regular dataclass must not be flagged, got: {issues!r}"
|
|
@@ -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
|
+
|