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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Tests for check_test_branching_in_production — flags prod-vs-test branching.
|
|
2
|
+
|
|
3
|
+
Per Plan 1c.di_pattern_check / Phase B3: production code that branches on
|
|
4
|
+
TESTING / PYTEST_CURRENT_TEST via os.environ creates two parallel
|
|
5
|
+
implementations the wrong way. The correct pattern is dependency injection
|
|
6
|
+
(`_test_hooks.py` sibling) so production code is single-path and tests
|
|
7
|
+
override the dependency.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_enforcer_module() -> ModuleType:
|
|
18
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
19
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
20
|
+
assert spec is not None
|
|
21
|
+
assert spec.loader is not None
|
|
22
|
+
module = importlib.util.module_from_spec(spec)
|
|
23
|
+
spec.loader.exec_module(module)
|
|
24
|
+
return module
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_test_branching_in_production(content: str, file_path: str) -> list[str]:
|
|
31
|
+
return code_rules_enforcer.check_test_branching_in_production(content, file_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
PRODUCTION_FILE_PATH = "/project/src/services.py"
|
|
35
|
+
TEST_FILE_PATH = "/project/src/test_services.py"
|
|
36
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_should_flag_os_environ_get_testing_branch() -> None:
|
|
40
|
+
source = (
|
|
41
|
+
"import os\n"
|
|
42
|
+
"def fetch_user():\n"
|
|
43
|
+
" if os.environ.get('TESTING'):\n"
|
|
44
|
+
" return None\n"
|
|
45
|
+
" return real_fetch()\n"
|
|
46
|
+
)
|
|
47
|
+
issues = check_test_branching_in_production(source, PRODUCTION_FILE_PATH)
|
|
48
|
+
assert any("TESTING" in each for each in issues), (
|
|
49
|
+
f"Expected TESTING env branch flagged, got: {issues!r}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_flag_pytest_current_test_branch() -> None:
|
|
54
|
+
source = (
|
|
55
|
+
"import os\n"
|
|
56
|
+
"def fetch_user():\n"
|
|
57
|
+
" if 'PYTEST_CURRENT_TEST' in os.environ:\n"
|
|
58
|
+
" return None\n"
|
|
59
|
+
" return real_fetch()\n"
|
|
60
|
+
)
|
|
61
|
+
issues = check_test_branching_in_production(source, PRODUCTION_FILE_PATH)
|
|
62
|
+
assert any("PYTEST_CURRENT_TEST" in each for each in issues), (
|
|
63
|
+
f"Expected PYTEST_CURRENT_TEST branch flagged, got: {issues!r}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_should_flag_env_testing_check() -> None:
|
|
68
|
+
source = (
|
|
69
|
+
"import os\n"
|
|
70
|
+
"TESTING_FLAG = os.environ.get('TESTING') == '1'\n"
|
|
71
|
+
"def get_db():\n"
|
|
72
|
+
" if TESTING_FLAG:\n"
|
|
73
|
+
" return MockDB()\n"
|
|
74
|
+
" return RealDB()\n"
|
|
75
|
+
)
|
|
76
|
+
issues = check_test_branching_in_production(source, PRODUCTION_FILE_PATH)
|
|
77
|
+
assert any("TESTING" in each for each in issues), (
|
|
78
|
+
f"Expected TESTING env access flagged, got: {issues!r}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_should_flag_environ_subscript_testing() -> None:
|
|
83
|
+
source = (
|
|
84
|
+
"import os\n"
|
|
85
|
+
"def get_db():\n"
|
|
86
|
+
" if os.environ['TESTING']:\n"
|
|
87
|
+
" return None\n"
|
|
88
|
+
" return RealDB()\n"
|
|
89
|
+
)
|
|
90
|
+
issues = check_test_branching_in_production(source, PRODUCTION_FILE_PATH)
|
|
91
|
+
assert any("TESTING" in each for each in issues), (
|
|
92
|
+
f"Expected os.environ['TESTING'] flagged, got: {issues!r}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_should_not_flag_other_env_var_access() -> None:
|
|
97
|
+
source = (
|
|
98
|
+
"import os\n"
|
|
99
|
+
"def get_db():\n"
|
|
100
|
+
" db_url = os.environ.get('DATABASE_URL')\n"
|
|
101
|
+
" return RealDB(db_url)\n"
|
|
102
|
+
)
|
|
103
|
+
issues = check_test_branching_in_production(source, PRODUCTION_FILE_PATH)
|
|
104
|
+
assert issues == [], f"Non-test env vars must not trigger, got: {issues!r}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_should_skip_test_file() -> None:
|
|
108
|
+
source = (
|
|
109
|
+
"import os\n"
|
|
110
|
+
"def fetch_user():\n"
|
|
111
|
+
" if os.environ.get('TESTING'):\n"
|
|
112
|
+
" return None\n"
|
|
113
|
+
)
|
|
114
|
+
issues = check_test_branching_in_production(source, TEST_FILE_PATH)
|
|
115
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
119
|
+
source = (
|
|
120
|
+
"import os\n"
|
|
121
|
+
"def fetch_user():\n"
|
|
122
|
+
" if os.environ.get('TESTING'):\n"
|
|
123
|
+
" return None\n"
|
|
124
|
+
)
|
|
125
|
+
issues = check_test_branching_in_production(source, HOOK_INFRASTRUCTURE_PATH)
|
|
126
|
+
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
130
|
+
source = "def fetch_user(\n"
|
|
131
|
+
issues = check_test_branching_in_production(source, PRODUCTION_FILE_PATH)
|
|
132
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_should_include_line_number_in_issue() -> None:
|
|
136
|
+
source = (
|
|
137
|
+
"import os\n\ndef fetch():\n if os.environ.get('TESTING'):\n pass\n"
|
|
138
|
+
)
|
|
139
|
+
issues = check_test_branching_in_production(source, PRODUCTION_FILE_PATH)
|
|
140
|
+
assert len(issues) >= 1
|
|
141
|
+
assert any("Line 4" in each for each in issues), (
|
|
142
|
+
f"Issue must include line number, got: {issues!r}"
|
|
143
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Tests for check_thin_wrapper_files — flag re-export-only modules.
|
|
2
|
+
|
|
3
|
+
Per Plan / Phase B5: a non-`__init__.py` module whose entire body is
|
|
4
|
+
`import` statements plus an `__all__` assignment is a thin wrapper that
|
|
5
|
+
forces callers through an indirection layer with no payload. Callers
|
|
6
|
+
should import from the real module. `__init__.py` is the canonical
|
|
7
|
+
re-export surface and is exempt.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_enforcer_module() -> ModuleType:
|
|
18
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
19
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
20
|
+
assert spec is not None
|
|
21
|
+
assert spec.loader is not None
|
|
22
|
+
module = importlib.util.module_from_spec(spec)
|
|
23
|
+
spec.loader.exec_module(module)
|
|
24
|
+
return module
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_thin_wrapper_files(content: str, file_path: str) -> list[str]:
|
|
31
|
+
return code_rules_enforcer.check_thin_wrapper_files(content, file_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
PRODUCTION_FILE_PATH = "/project/src/aliases.py"
|
|
35
|
+
INIT_FILE_PATH = "/project/src/__init__.py"
|
|
36
|
+
TEST_FILE_PATH = "/project/src/test_aliases.py"
|
|
37
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
38
|
+
CONFIG_FILE_PATH = "/project/config/aliases.py"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_should_flag_thin_wrapper_with_imports_and_all() -> None:
|
|
42
|
+
source = (
|
|
43
|
+
"from real_module import do_thing, other_thing\n"
|
|
44
|
+
"\n"
|
|
45
|
+
'__all__ = ["do_thing", "other_thing"]\n'
|
|
46
|
+
)
|
|
47
|
+
issues = check_thin_wrapper_files(source, PRODUCTION_FILE_PATH)
|
|
48
|
+
assert any("thin wrapper" in each.lower() for each in issues), (
|
|
49
|
+
f"Expected thin-wrapper flag, got: {issues!r}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_flag_thin_wrapper_imports_only_no_all() -> None:
|
|
54
|
+
source = "from real_module import do_thing\nfrom other_module import other_thing\n"
|
|
55
|
+
issues = check_thin_wrapper_files(source, PRODUCTION_FILE_PATH)
|
|
56
|
+
assert any("thin wrapper" in each.lower() for each in issues), (
|
|
57
|
+
f"Expected import-only thin-wrapper flag, got: {issues!r}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_should_not_flag_init_file() -> None:
|
|
62
|
+
source = 'from real_module import do_thing\n\n__all__ = ["do_thing"]\n'
|
|
63
|
+
issues = check_thin_wrapper_files(source, INIT_FILE_PATH)
|
|
64
|
+
assert issues == [], (
|
|
65
|
+
f"__init__.py is the canonical re-export surface, got: {issues!r}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_should_not_flag_file_with_function_definition() -> None:
|
|
70
|
+
source = (
|
|
71
|
+
"from real_module import dependency\n"
|
|
72
|
+
"\n"
|
|
73
|
+
'__all__ = ["public_helper"]\n'
|
|
74
|
+
"\n"
|
|
75
|
+
"def public_helper(value: int) -> int:\n"
|
|
76
|
+
" return dependency(value)\n"
|
|
77
|
+
)
|
|
78
|
+
issues = check_thin_wrapper_files(source, PRODUCTION_FILE_PATH)
|
|
79
|
+
assert issues == [], f"File with real code must not be flagged, got: {issues!r}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_should_not_flag_file_with_class_definition() -> None:
|
|
83
|
+
source = "from real_module import Base\n\nclass Subtype(Base):\n pass\n"
|
|
84
|
+
issues = check_thin_wrapper_files(source, PRODUCTION_FILE_PATH)
|
|
85
|
+
assert issues == [], f"File with class must not be flagged, got: {issues!r}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_should_not_flag_file_with_constant_assignment() -> None:
|
|
89
|
+
source = (
|
|
90
|
+
"from real_module import constant_value\n\nDERIVED_VALUE = constant_value * 2\n"
|
|
91
|
+
)
|
|
92
|
+
issues = check_thin_wrapper_files(source, PRODUCTION_FILE_PATH)
|
|
93
|
+
assert issues == [], (
|
|
94
|
+
f"File with derived constant must not be flagged, got: {issues!r}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_should_skip_test_file() -> None:
|
|
99
|
+
source = "from real_module import do_thing\n\n__all__ = ['do_thing']\n"
|
|
100
|
+
issues = check_thin_wrapper_files(source, TEST_FILE_PATH)
|
|
101
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
105
|
+
source = "from real_module import do_thing\n\n__all__ = ['do_thing']\n"
|
|
106
|
+
issues = check_thin_wrapper_files(source, HOOK_INFRASTRUCTURE_PATH)
|
|
107
|
+
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_should_skip_config_file() -> None:
|
|
111
|
+
source = "from real_module import do_thing\n\n__all__ = ['do_thing']\n"
|
|
112
|
+
issues = check_thin_wrapper_files(source, CONFIG_FILE_PATH)
|
|
113
|
+
assert issues == [], f"config/ files exempt, got: {issues!r}"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
117
|
+
source = "from real_module import (\n"
|
|
118
|
+
issues = check_thin_wrapper_files(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_empty_file() -> None:
|
|
123
|
+
issues = check_thin_wrapper_files("", PRODUCTION_FILE_PATH)
|
|
124
|
+
assert issues == [], f"Empty file must not be flagged, got: {issues!r}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_should_not_flag_module_docstring_with_real_code() -> None:
|
|
128
|
+
source = (
|
|
129
|
+
'"""Module docstring."""\n'
|
|
130
|
+
"from real_module import dependency\n"
|
|
131
|
+
"\n"
|
|
132
|
+
"def helper() -> int:\n"
|
|
133
|
+
" return dependency()\n"
|
|
134
|
+
)
|
|
135
|
+
issues = check_thin_wrapper_files(source, PRODUCTION_FILE_PATH)
|
|
136
|
+
assert issues == [], f"Docstring + real code must not be flagged, got: {issues!r}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_should_flag_thin_wrapper_with_module_docstring() -> None:
|
|
140
|
+
source = (
|
|
141
|
+
'"""Module docstring."""\n'
|
|
142
|
+
"from real_module import do_thing\n"
|
|
143
|
+
"\n"
|
|
144
|
+
'__all__ = ["do_thing"]\n'
|
|
145
|
+
)
|
|
146
|
+
issues = check_thin_wrapper_files(source, PRODUCTION_FILE_PATH)
|
|
147
|
+
assert any("thin wrapper" in each.lower() for each in issues), (
|
|
148
|
+
f"Docstring + import + __all__ is still a thin wrapper, got: {issues!r}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_validate_content_uses_empty_full_file_content_over_pre_edit_fragment() -> None:
|
|
153
|
+
"""An empty-string `full_file_content` must be honored, not silently replaced with `content`.
|
|
154
|
+
|
|
155
|
+
Regression for loop1-8: the `or` short-circuit at line 3775 collapsed
|
|
156
|
+
empty-string and None, so an Edit that produced an empty post-edit file
|
|
157
|
+
was scanned against the pre-edit fragment instead of the empty file.
|
|
158
|
+
The thin-wrapper check uses the same idiom — an empty post-edit file is
|
|
159
|
+
not a thin wrapper, but a pre-edit fragment with imports + __all__ is.
|
|
160
|
+
"""
|
|
161
|
+
pre_edit_fragment = "from real_module import do_thing\n__all__ = ['do_thing']\n"
|
|
162
|
+
issues = code_rules_enforcer.validate_content(
|
|
163
|
+
pre_edit_fragment,
|
|
164
|
+
PRODUCTION_FILE_PATH,
|
|
165
|
+
full_file_content="",
|
|
166
|
+
)
|
|
167
|
+
assert not any("thin wrapper" in each.lower() for each in issues), (
|
|
168
|
+
f"empty post-edit file must not be flagged as a thin wrapper, got: {issues!r}"
|
|
169
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Tests covering TODO/FIXME/HACK/XXX exemption from the comment blocker.
|
|
2
|
+
|
|
3
|
+
CODE_RULES.md requires scaffolding/placeholder code to carry TODO comments
|
|
4
|
+
naming what replaces it and why. Without an exemption, the no-NEW-comments
|
|
5
|
+
rule blocks every authored TODO. These tests pin the exemption.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_enforcer_module() -> ModuleType:
|
|
16
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
17
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
18
|
+
assert spec is not None
|
|
19
|
+
assert spec.loader is not None
|
|
20
|
+
module = importlib.util.module_from_spec(spec)
|
|
21
|
+
spec.loader.exec_module(module)
|
|
22
|
+
return module
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_python_check_should_exempt_standalone_todo_comment() -> None:
|
|
29
|
+
content = "# TODO: replace stub with real implementation\nx = 1\n"
|
|
30
|
+
issues = code_rules_enforcer.check_comments_python(content)
|
|
31
|
+
assert issues == [], f"Expected no issues for '# TODO:' but got: {issues!r}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_python_check_should_exempt_standalone_fixme_comment() -> None:
|
|
35
|
+
content = "# FIXME: handle the empty list case\nx = 1\n"
|
|
36
|
+
issues = code_rules_enforcer.check_comments_python(content)
|
|
37
|
+
assert issues == [], f"Expected no issues for '# FIXME:' but got: {issues!r}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_python_check_should_exempt_standalone_hack_comment() -> None:
|
|
41
|
+
content = "# HACK: working around upstream bug #1234\nx = 1\n"
|
|
42
|
+
issues = code_rules_enforcer.check_comments_python(content)
|
|
43
|
+
assert issues == [], f"Expected no issues for '# HACK:' but got: {issues!r}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_python_check_should_exempt_standalone_xxx_comment() -> None:
|
|
47
|
+
content = "# XXX revisit when API stabilizes\nx = 1\n"
|
|
48
|
+
issues = code_rules_enforcer.check_comments_python(content)
|
|
49
|
+
assert issues == [], f"Expected no issues for '# XXX' but got: {issues!r}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_python_check_should_exempt_inline_todo_comment() -> None:
|
|
53
|
+
content = "x = 1 # TODO: extract to config\n"
|
|
54
|
+
issues = code_rules_enforcer.check_comments_python(content)
|
|
55
|
+
assert issues == [], f"Expected no issues for inline TODO but got: {issues!r}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_python_check_should_exempt_inline_fixme_comment() -> None:
|
|
59
|
+
content = "x = 1 # FIXME: race condition\n"
|
|
60
|
+
issues = code_rules_enforcer.check_comments_python(content)
|
|
61
|
+
assert issues == [], f"Expected no issues for inline FIXME but got: {issues!r}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_javascript_check_should_exempt_inline_todo_comment() -> None:
|
|
65
|
+
content = "const x = 1; // TODO: replace with config\n"
|
|
66
|
+
issues = code_rules_enforcer.check_comments_javascript(content)
|
|
67
|
+
assert issues == [], f"Expected no issues for JS inline TODO but got: {issues!r}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_javascript_check_should_exempt_inline_fixme_comment() -> None:
|
|
71
|
+
content = "const x = 1; // FIXME: handle null case\n"
|
|
72
|
+
issues = code_rules_enforcer.check_comments_javascript(content)
|
|
73
|
+
assert issues == [], f"Expected no issues for JS inline FIXME but got: {issues!r}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_extract_comment_texts_should_skip_standalone_todo() -> None:
|
|
77
|
+
content = "# TODO: replace stub\nx = 1\n"
|
|
78
|
+
inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
|
|
79
|
+
assert "# TODO: replace stub" not in standalone, (
|
|
80
|
+
"Standalone TODO must not be tracked as a comment subject to "
|
|
81
|
+
f"deletion-blocking, got standalone={standalone!r}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_extract_comment_texts_should_skip_inline_todo() -> None:
|
|
86
|
+
content = "x = 1 # TODO: extract to config\n"
|
|
87
|
+
inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
|
|
88
|
+
assert all("TODO" not in each_comment for each_comment in inline), (
|
|
89
|
+
"Inline TODO must not be tracked as a comment subject to "
|
|
90
|
+
f"deletion-blocking, got inline={inline!r}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_existing_non_todo_comment_still_flagged() -> None:
|
|
95
|
+
content = "x = 1 # this is just a comment\n"
|
|
96
|
+
issues = code_rules_enforcer.check_comments_python(content)
|
|
97
|
+
assert len(issues) == 1, (
|
|
98
|
+
f"Expected non-TODO inline comment to still be flagged, got: {issues!r}"
|
|
99
|
+
)
|
|
@@ -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}"
|