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
|
@@ -76,6 +76,56 @@ def test_should_flag_pep604_union_dict_parameter() -> None:
|
|
|
76
76
|
)
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
def test_should_not_flag_bare_dict_parameter() -> None:
|
|
80
|
+
source = "def consume(user_record: dict) -> None:\n return None\n"
|
|
81
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
82
|
+
assert not any(
|
|
83
|
+
"Collection parameter user_record -" in each_issue for each_issue in issues
|
|
84
|
+
), (
|
|
85
|
+
f"Bare dict parameter is a structured record, not a collection, got: {issues}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_should_flag_subscripted_dict_parameter() -> None:
|
|
90
|
+
source = "def consume(price_lookup: dict[str, int]) -> None:\n return None\n"
|
|
91
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
92
|
+
assert any(
|
|
93
|
+
"Collection parameter price_lookup -" in each_issue for each_issue in issues
|
|
94
|
+
), (
|
|
95
|
+
f"Expected dict[K, V] parameter flagged, got: {issues}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_should_not_flag_module_level_bare_dict_constant() -> None:
|
|
100
|
+
source = "MY_USER_RECORD: dict = {}\n"
|
|
101
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
102
|
+
assert not any(
|
|
103
|
+
"Collection constant MY_USER_RECORD -" in each_issue for each_issue in issues
|
|
104
|
+
), (
|
|
105
|
+
f"Bare dict module-level constant is a structured record, not a collection, got: {issues}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_should_flag_module_level_subscripted_dict_constant_without_all_prefix() -> None:
|
|
110
|
+
source = "MY_PRICE_LOOKUP: dict[str, int] = {}\n"
|
|
111
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
112
|
+
assert any(
|
|
113
|
+
"Collection constant MY_PRICE_LOOKUP -" in each_issue for each_issue in issues
|
|
114
|
+
), (
|
|
115
|
+
f"Expected dict[K, V] module-level constant flagged, got: {issues}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_should_not_flag_pep604_bare_dict_union_parameter() -> None:
|
|
120
|
+
source = "def consume(user_record: dict | None = None) -> None:\n return None\n"
|
|
121
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
122
|
+
assert not any(
|
|
123
|
+
"Collection parameter user_record -" in each_issue for each_issue in issues
|
|
124
|
+
), (
|
|
125
|
+
f"Bare dict | None parameter is a structured record, not a collection, got: {issues}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
79
129
|
def test_should_not_flag_pep604_union_when_param_has_all_prefix() -> None:
|
|
80
130
|
source = (
|
|
81
131
|
"def consume(all_numbers: set[int] | None = None) -> None:\n return None\n"
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Tests for check_docstring_format — Google-style Args:/Returns:/Raises:.
|
|
2
|
+
|
|
3
|
+
Per Plan 1c.docstring_format_check / Phase B7: a public function whose
|
|
4
|
+
signature takes parameters, returns a non-None value, or raises an
|
|
5
|
+
exception must document those facts in Google-style sections so
|
|
6
|
+
callers can reason about the contract without reading the body.
|
|
7
|
+
|
|
8
|
+
Exemptions: private (`_foo`), dunder (`__init__`), `@property`,
|
|
9
|
+
functions ≤3 lines (trivial), abstract methods, test files.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib.util
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_enforcer_module() -> ModuleType:
|
|
20
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
21
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
22
|
+
assert spec is not None
|
|
23
|
+
assert spec.loader is not None
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
spec.loader.exec_module(module)
|
|
26
|
+
return module
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_docstring_format(content: str, file_path: str) -> list[str]:
|
|
33
|
+
return code_rules_enforcer.check_docstring_format(content, file_path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
PRODUCTION_FILE_PATH = "/project/src/services.py"
|
|
37
|
+
TEST_FILE_PATH = "/project/src/test_services.py"
|
|
38
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _function_with_param_no_docstring() -> str:
|
|
42
|
+
return (
|
|
43
|
+
"def fetch_user(user_id: int) -> str:\n"
|
|
44
|
+
" lookup = _registry.get(user_id)\n"
|
|
45
|
+
" if not lookup:\n"
|
|
46
|
+
" return ''\n"
|
|
47
|
+
" return lookup.name\n"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_should_flag_public_function_with_params_missing_args_section() -> None:
|
|
52
|
+
issues = check_docstring_format(
|
|
53
|
+
_function_with_param_no_docstring(), PRODUCTION_FILE_PATH
|
|
54
|
+
)
|
|
55
|
+
assert any("Args" in each for each in issues), (
|
|
56
|
+
f"Expected missing-Args flag, got: {issues!r}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_should_flag_public_function_with_non_none_return_missing_returns_section() -> (
|
|
61
|
+
None
|
|
62
|
+
):
|
|
63
|
+
source = (
|
|
64
|
+
"def fetch_user(user_id: int) -> str:\n"
|
|
65
|
+
' """Look up a user by id.\n'
|
|
66
|
+
"\n"
|
|
67
|
+
" Args:\n"
|
|
68
|
+
" user_id: The user identifier.\n"
|
|
69
|
+
' """\n'
|
|
70
|
+
" lookup = _registry.get(user_id)\n"
|
|
71
|
+
" if not lookup:\n"
|
|
72
|
+
" return ''\n"
|
|
73
|
+
" return lookup.name\n"
|
|
74
|
+
)
|
|
75
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
76
|
+
assert any("Returns" in each or "Yields" in each for each in issues), (
|
|
77
|
+
f"Expected missing-Returns flag, got: {issues!r}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_should_flag_public_function_with_raise_missing_raises_section() -> None:
|
|
82
|
+
source = (
|
|
83
|
+
"def fetch_user(user_id: int) -> str:\n"
|
|
84
|
+
' """Look up a user by id.\n'
|
|
85
|
+
"\n"
|
|
86
|
+
" Args:\n"
|
|
87
|
+
" user_id: The user identifier.\n"
|
|
88
|
+
"\n"
|
|
89
|
+
" Returns:\n"
|
|
90
|
+
" The user name.\n"
|
|
91
|
+
' """\n'
|
|
92
|
+
" lookup = _registry.get(user_id)\n"
|
|
93
|
+
" if not lookup:\n"
|
|
94
|
+
" raise LookupError('missing')\n"
|
|
95
|
+
" return lookup.name\n"
|
|
96
|
+
)
|
|
97
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
98
|
+
assert any("Raises" in each for each in issues), (
|
|
99
|
+
f"Expected missing-Raises flag, got: {issues!r}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_should_not_flag_function_with_complete_google_docstring() -> None:
|
|
104
|
+
source = (
|
|
105
|
+
"def fetch_user(user_id: int) -> str:\n"
|
|
106
|
+
' """Look up a user by id.\n'
|
|
107
|
+
"\n"
|
|
108
|
+
" Args:\n"
|
|
109
|
+
" user_id: The user identifier.\n"
|
|
110
|
+
"\n"
|
|
111
|
+
" Returns:\n"
|
|
112
|
+
" The user name.\n"
|
|
113
|
+
"\n"
|
|
114
|
+
" Raises:\n"
|
|
115
|
+
" LookupError: When the user is missing.\n"
|
|
116
|
+
' """\n'
|
|
117
|
+
" lookup = _registry.get(user_id)\n"
|
|
118
|
+
" if not lookup:\n"
|
|
119
|
+
" raise LookupError('missing')\n"
|
|
120
|
+
" return lookup.name\n"
|
|
121
|
+
)
|
|
122
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
123
|
+
assert issues == [], f"Complete docstring must not be flagged, got: {issues!r}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_should_not_require_returns_when_return_type_is_none() -> None:
|
|
127
|
+
source = (
|
|
128
|
+
"def store_user(user_id: int) -> None:\n"
|
|
129
|
+
' """Persist a user record.\n'
|
|
130
|
+
"\n"
|
|
131
|
+
" Args:\n"
|
|
132
|
+
" user_id: The user identifier.\n"
|
|
133
|
+
' """\n'
|
|
134
|
+
" if user_id < 0:\n"
|
|
135
|
+
" return\n"
|
|
136
|
+
" _registry[user_id] = True\n"
|
|
137
|
+
)
|
|
138
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
139
|
+
assert issues == [], (
|
|
140
|
+
f"None-returning function must not require Returns:, got: {issues!r}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_should_accept_yields_in_lieu_of_returns_for_generator() -> None:
|
|
145
|
+
source = (
|
|
146
|
+
'def stream_users(batch_size: int) -> "Iterator[str]":\n'
|
|
147
|
+
' """Stream user names lazily.\n'
|
|
148
|
+
"\n"
|
|
149
|
+
" Args:\n"
|
|
150
|
+
" batch_size: How many to read at a time.\n"
|
|
151
|
+
"\n"
|
|
152
|
+
" Yields:\n"
|
|
153
|
+
" Each user name in turn.\n"
|
|
154
|
+
' """\n'
|
|
155
|
+
" for each in _registry.values():\n"
|
|
156
|
+
" yield each.name\n"
|
|
157
|
+
)
|
|
158
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
159
|
+
assert issues == [], f"Yields: must satisfy Returns: requirement, got: {issues!r}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_should_skip_private_function() -> None:
|
|
163
|
+
source = "def _internal_helper(value: int) -> int:\n return value * 2\n"
|
|
164
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
165
|
+
assert issues == [], f"Private functions exempt, got: {issues!r}"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_should_skip_dunder_method() -> None:
|
|
169
|
+
source = (
|
|
170
|
+
"class Cache:\n"
|
|
171
|
+
" def __init__(self, capacity: int) -> None:\n"
|
|
172
|
+
" self._capacity = capacity\n"
|
|
173
|
+
" self._storage = {}\n"
|
|
174
|
+
" self._hits = 0\n"
|
|
175
|
+
" self._misses = 0\n"
|
|
176
|
+
)
|
|
177
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
178
|
+
assert issues == [], f"Dunder methods exempt, got: {issues!r}"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_should_skip_property_method() -> None:
|
|
182
|
+
source = (
|
|
183
|
+
"class Cache:\n"
|
|
184
|
+
" @property\n"
|
|
185
|
+
" def capacity(self) -> int:\n"
|
|
186
|
+
" first_calculation = self._capacity\n"
|
|
187
|
+
" adjusted = first_calculation - self._reserved\n"
|
|
188
|
+
" return adjusted\n"
|
|
189
|
+
)
|
|
190
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
191
|
+
assert issues == [], f"@property methods exempt, got: {issues!r}"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_should_skip_short_function() -> None:
|
|
195
|
+
source = "def double(value: int) -> int:\n return value * 2\n"
|
|
196
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
197
|
+
assert issues == [], f"Functions <=3 lines exempt, got: {issues!r}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_should_skip_abstract_method() -> None:
|
|
201
|
+
source = (
|
|
202
|
+
"from abc import abstractmethod\n"
|
|
203
|
+
"\n"
|
|
204
|
+
"class Repository:\n"
|
|
205
|
+
" @abstractmethod\n"
|
|
206
|
+
" def fetch(self, key: str) -> int:\n"
|
|
207
|
+
" ...\n"
|
|
208
|
+
)
|
|
209
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
210
|
+
assert issues == [], f"@abstractmethod exempt, got: {issues!r}"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_should_skip_test_file() -> None:
|
|
214
|
+
issues = check_docstring_format(_function_with_param_no_docstring(), TEST_FILE_PATH)
|
|
215
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
219
|
+
issues = check_docstring_format(
|
|
220
|
+
_function_with_param_no_docstring(), HOOK_INFRASTRUCTURE_PATH
|
|
221
|
+
)
|
|
222
|
+
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
226
|
+
issues = check_docstring_format("def fetch(\n", PRODUCTION_FILE_PATH)
|
|
227
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_should_skip_short_function_with_docstring() -> None:
|
|
231
|
+
source = (
|
|
232
|
+
"def double(value: int) -> int:\n"
|
|
233
|
+
' """Multiply the value by two.\n'
|
|
234
|
+
"\n"
|
|
235
|
+
" The calculation uses left-shift internally.\n"
|
|
236
|
+
' """\n'
|
|
237
|
+
" return value * 2\n"
|
|
238
|
+
)
|
|
239
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
240
|
+
assert issues == [], (
|
|
241
|
+
f"Short function with docstring must be exempt, got: {issues!r}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_should_not_count_self_or_cls_as_documentable_params() -> None:
|
|
246
|
+
source = (
|
|
247
|
+
"class Cache:\n"
|
|
248
|
+
" def reset(self) -> None:\n"
|
|
249
|
+
' """Drop all cached entries."""\n'
|
|
250
|
+
" self._storage.clear()\n"
|
|
251
|
+
" self._hits = 0\n"
|
|
252
|
+
" self._misses = 0\n"
|
|
253
|
+
)
|
|
254
|
+
issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
|
|
255
|
+
assert issues == [], f"self-only methods must not require Args:, got: {issues!r}"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Unit tests for the inline-tuple snake_case-string-magic check in code_rules_enforcer.
|
|
2
|
+
|
|
3
|
+
These tests cover the gap surfaced during PR #419: a tuple literal whose first
|
|
4
|
+
element is a snake_case string (e.g. ``("kept", "Unknown status")``) inside a
|
|
5
|
+
function body slipped past the Write/Edit hook even though the commit-time
|
|
6
|
+
gate caught it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
import pathlib
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
14
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
16
|
+
|
|
17
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
18
|
+
"code_rules_enforcer",
|
|
19
|
+
_HOOK_DIR / "code_rules_enforcer.py",
|
|
20
|
+
)
|
|
21
|
+
assert hook_spec is not None
|
|
22
|
+
assert hook_spec.loader is not None
|
|
23
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
24
|
+
hook_spec.loader.exec_module(hook_module)
|
|
25
|
+
check_inline_tuple_string_magic = hook_module.check_inline_tuple_string_magic
|
|
26
|
+
validate_content = hook_module.validate_content
|
|
27
|
+
|
|
28
|
+
PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
|
|
29
|
+
TEST_FILE_PATH = "packages/app/services/test_loader.py"
|
|
30
|
+
CONFIG_FILE_PATH = "packages/app/config/labels.py"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_should_flag_inline_snake_case_tuple_pair_inside_function() -> None:
|
|
34
|
+
content = "def describe(glyph):\n return {'a': ('kept', 'Unknown status')}\n"
|
|
35
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
36
|
+
assert any("'kept'" in each_issue for each_issue in issues), (
|
|
37
|
+
f"Expected 'kept' tuple-pair flagged, got: {issues}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_should_flag_inline_snake_case_tuple_inside_dict_value() -> None:
|
|
42
|
+
content = "def lookup():\n return {'STATUS_KEPT': ('kept', 'Patch unchanged')}\n"
|
|
43
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
44
|
+
assert any("'kept'" in each_issue for each_issue in issues), (
|
|
45
|
+
f"Expected nested tuple flagged, got: {issues}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_should_flag_first_element_snake_case_with_underscore() -> None:
|
|
50
|
+
content = "def label():\n return ('unknown_status', 'placeholder')\n"
|
|
51
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
52
|
+
assert any("'unknown_status'" in each_issue for each_issue in issues), (
|
|
53
|
+
f"Expected snake_case-with-underscore flagged, got: {issues}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_should_not_flag_tuple_outside_function_body() -> None:
|
|
58
|
+
content = "ALL_STATUS = ('kept', 'Unknown status')\n"
|
|
59
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
60
|
+
assert issues == [], (
|
|
61
|
+
f"Module-level constants are not in function bodies; must not flag, got: {issues}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_should_skip_test_files() -> None:
|
|
66
|
+
content = "def test_thing():\n return ('kept', 'Unknown status')\n"
|
|
67
|
+
issues = check_inline_tuple_string_magic(content, TEST_FILE_PATH)
|
|
68
|
+
assert issues == [], f"Test files are exempt, got: {issues}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_should_skip_config_files() -> None:
|
|
72
|
+
content = "def build():\n return ('kept', 'Unknown status')\n"
|
|
73
|
+
issues = check_inline_tuple_string_magic(content, CONFIG_FILE_PATH)
|
|
74
|
+
assert issues == [], f"Config files are exempt, got: {issues}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_should_not_flag_tuple_with_non_snake_case_first_element() -> None:
|
|
78
|
+
content = "def render():\n return ('Title Case', 'Body')\n"
|
|
79
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
80
|
+
assert issues == [], (
|
|
81
|
+
f"Non-snake-case strings are not column/key-like; must not flag, got: {issues}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_should_not_flag_short_string_first_element() -> None:
|
|
86
|
+
content = "def render():\n return ('ok', 'fine')\n"
|
|
87
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
88
|
+
assert issues == [], (
|
|
89
|
+
f"Strings shorter than minimum length are exempt, got: {issues}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_should_not_flag_keyword_strings() -> None:
|
|
94
|
+
content = "def render():\n return ('true', 'false')\n"
|
|
95
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
96
|
+
assert issues == [], (
|
|
97
|
+
f"Keyword literals (true/false/none/null) are exempt, got: {issues}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_should_not_flag_tuple_longer_than_pair() -> None:
|
|
102
|
+
content = "def render():\n return ('kept', 'Unknown status', 'extra')\n"
|
|
103
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
104
|
+
assert issues == [], f"Only two-element tuples are inspected, got: {issues}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_should_dedupe_nested_function_tuples() -> None:
|
|
108
|
+
"""Tuples inside nested FunctionDefs must produce one finding, not many.
|
|
109
|
+
|
|
110
|
+
Without deduplication the outer ast.walk enumerates every FunctionDef
|
|
111
|
+
including nested ones, then the inner walk visits each tuple via every
|
|
112
|
+
enclosing function. Must surface exactly one finding per tuple site.
|
|
113
|
+
"""
|
|
114
|
+
content = (
|
|
115
|
+
"def outer():\n"
|
|
116
|
+
" def inner():\n"
|
|
117
|
+
' x = ("some_column_name", 42)\n'
|
|
118
|
+
" return x\n"
|
|
119
|
+
" return inner\n"
|
|
120
|
+
)
|
|
121
|
+
issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
|
|
122
|
+
assert len(issues) == 1, f"expected 1 finding, got {len(issues)}: {issues!r}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_validate_content_wires_check_for_python_files() -> None:
|
|
126
|
+
content = "def describe(glyph):\n return {'a': ('kept', 'Unknown status')}\n"
|
|
127
|
+
issues = validate_content(content, PRODUCTION_FILE_PATH)
|
|
128
|
+
assert any("'kept'" in each_issue for each_issue in issues), (
|
|
129
|
+
f"validate_content must run the new check, got: {issues}"
|
|
130
|
+
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Tests for check_stub_implementations — flags placeholder function bodies.
|
|
2
|
+
|
|
3
|
+
Per Plan 1c.stub_detector / Phase B1: production functions whose body is
|
|
4
|
+
only `pass`, `...` (Ellipsis), or `raise NotImplementedError` are stubs.
|
|
5
|
+
Exemptions: ABC methods, Protocol methods, abstractmethod-decorated
|
|
6
|
+
functions, test files, hook infrastructure.
|
|
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_stub_implementations(content: str, file_path: str) -> list[str]:
|
|
30
|
+
return code_rules_enforcer.check_stub_implementations(content, file_path)
|
|
31
|
+
|
|
32
|
+
PRODUCTION_FILE_PATH = "/project/src/services.py"
|
|
33
|
+
TEST_FILE_PATH = "/project/src/test_services.py"
|
|
34
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_should_flag_pass_only_function() -> None:
|
|
38
|
+
source = "def parse_invoice(payload: str) -> int:\n pass\n"
|
|
39
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
40
|
+
assert any("parse_invoice" in each for each in issues), (
|
|
41
|
+
f"Expected pass-only function to be flagged, got: {issues!r}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_should_flag_ellipsis_only_function() -> None:
|
|
46
|
+
source = "def parse_invoice(payload: str) -> int:\n ...\n"
|
|
47
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
48
|
+
assert any("parse_invoice" in each for each in issues), (
|
|
49
|
+
f"Expected ellipsis-only function to be flagged, got: {issues!r}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_flag_raise_not_implemented_function() -> None:
|
|
54
|
+
source = "def parse_invoice(payload: str) -> int:\n raise NotImplementedError\n"
|
|
55
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
56
|
+
assert any("parse_invoice" in each for each in issues), (
|
|
57
|
+
f"Expected NotImplementedError stub to be flagged, got: {issues!r}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_should_flag_raise_not_implemented_with_message() -> None:
|
|
62
|
+
source = (
|
|
63
|
+
"def parse_invoice(payload: str) -> int:\n"
|
|
64
|
+
" raise NotImplementedError('coming soon')\n"
|
|
65
|
+
)
|
|
66
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
67
|
+
assert any("parse_invoice" in each for each in issues), (
|
|
68
|
+
f"Expected NotImplementedError(...) stub to be flagged, got: {issues!r}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_should_flag_function_with_docstring_then_pass() -> None:
|
|
73
|
+
source = (
|
|
74
|
+
"def parse_invoice(payload: str) -> int:\n"
|
|
75
|
+
' """Parse the invoice."""\n'
|
|
76
|
+
" pass\n"
|
|
77
|
+
)
|
|
78
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
79
|
+
assert any("parse_invoice" in each for each in issues), (
|
|
80
|
+
f"Docstring + pass is still a stub; should be flagged. Got: {issues!r}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_should_not_flag_real_implementation() -> None:
|
|
85
|
+
source = "def parse_invoice(payload: str) -> int:\n return len(payload)\n"
|
|
86
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
87
|
+
assert issues == [], f"Real impl must not be flagged, got: {issues!r}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_should_exempt_abstractmethod_decorated() -> None:
|
|
91
|
+
source = (
|
|
92
|
+
"from abc import ABC, abstractmethod\n"
|
|
93
|
+
"class InvoiceParser(ABC):\n"
|
|
94
|
+
" @abstractmethod\n"
|
|
95
|
+
" def parse(self, payload: str) -> int:\n"
|
|
96
|
+
" pass\n"
|
|
97
|
+
)
|
|
98
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
99
|
+
assert issues == [], f"@abstractmethod must be exempt, got: {issues!r}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_should_exempt_protocol_methods() -> None:
|
|
103
|
+
source = (
|
|
104
|
+
"from typing import Protocol\n"
|
|
105
|
+
"class InvoiceParser(Protocol):\n"
|
|
106
|
+
" def parse(self, payload: str) -> int:\n"
|
|
107
|
+
" ...\n"
|
|
108
|
+
)
|
|
109
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
110
|
+
assert issues == [], f"Protocol methods must be exempt, got: {issues!r}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_should_skip_test_file() -> None:
|
|
114
|
+
source = "def stub_helper():\n pass\n"
|
|
115
|
+
issues = check_stub_implementations(source, TEST_FILE_PATH)
|
|
116
|
+
assert issues == [], f"Test files must be exempt, got: {issues!r}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
120
|
+
source = "def stub_helper():\n pass\n"
|
|
121
|
+
issues = check_stub_implementations(source, HOOK_INFRASTRUCTURE_PATH)
|
|
122
|
+
assert issues == [], f"Hook infrastructure must be exempt, got: {issues!r}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
126
|
+
source = "def parse_invoice(\n"
|
|
127
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
128
|
+
assert issues == [], f"Syntax errors must yield no issues, got: {issues!r}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_should_include_line_number_in_issue() -> None:
|
|
132
|
+
source = "x = 1\n\ndef parse_invoice():\n pass\n"
|
|
133
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
134
|
+
assert len(issues) == 1
|
|
135
|
+
assert "Line 3" in issues[0], f"Issue must include line number, got: {issues[0]!r}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_should_cap_at_three_issues() -> None:
|
|
139
|
+
source = "\n\n".join(f"def stub_{i}():\n pass" for i in range(5)) + "\n"
|
|
140
|
+
issues = check_stub_implementations(source, PRODUCTION_FILE_PATH)
|
|
141
|
+
assert len(issues) <= 3, f"Issue count must be capped at 3, got: {len(issues)}"
|