claude-dev-env 1.72.0 → 1.74.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 +2 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +2 -2
- package/bin/install.mjs +73 -5
- package/bin/install.test.mjs +360 -4
- package/hooks/blocking/CLAUDE.md +6 -1
- package/hooks/blocking/block_main_commit.py +14 -0
- package/hooks/blocking/bot_mention_comment_blocker.py +7 -0
- package/hooks/blocking/claude_md_orphan_file_blocker.py +19 -48
- package/hooks/blocking/code_rules_dead_config_field.py +69 -56
- package/hooks/blocking/code_rules_docstrings.py +839 -0
- package/hooks/blocking/code_rules_enforcer.py +38 -0
- package/hooks/blocking/code_rules_shared.py +19 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +426 -0
- package/hooks/blocking/convergence_gate_blocker.py +17 -3
- package/hooks/blocking/destructive_command_blocker.py +7 -0
- package/hooks/blocking/docstring_rule_gate_count_blocker.py +321 -0
- package/hooks/blocking/gh_body_arg_blocker.py +8 -0
- package/hooks/blocking/gh_pr_author_enforcer.py +7 -0
- package/hooks/blocking/hedging_language_blocker.py +16 -10
- package/hooks/blocking/hook_prose_detector_consistency.py +7 -0
- package/hooks/blocking/intent_only_ending_blocker.py +17 -11
- package/hooks/blocking/md_to_html_blocker.py +17 -10
- package/hooks/blocking/open_questions_in_plans_blocker.py +15 -8
- package/hooks/blocking/package_inventory_stale_blocker.py +398 -0
- package/hooks/blocking/plain_language_blocker.py +57 -16
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +11 -5
- package/hooks/blocking/pr_description_enforcer.py +6 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +545 -0
- package/hooks/blocking/precommit_code_rules_gate.py +10 -1
- package/hooks/blocking/pytest_testpaths_orphan_blocker.py +366 -0
- package/hooks/blocking/question_to_user_enforcer.py +18 -12
- package/hooks/blocking/send_user_file_open_locally_blocker.py +70 -0
- package/hooks/blocking/sensitive_file_protector.py +15 -1
- package/hooks/blocking/session_handoff_blocker.py +14 -8
- package/hooks/blocking/state_description_blocker.py +81 -36
- package/hooks/blocking/subprocess_budget_completeness.py +9 -3
- package/hooks/blocking/tdd_enforcer.py +6 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +81 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +93 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +207 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +262 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_undefined_constant.py +253 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +279 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +501 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +203 -0
- package/hooks/blocking/test_hook_block_logger_coverage.py +53 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +329 -0
- package/hooks/blocking/test_plain_language_blocker.py +36 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +816 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +341 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +247 -0
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +114 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +208 -0
- package/hooks/blocking/test_state_description_blocker.py +41 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +49 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +4 -19
- package/hooks/blocking/verdict_directory_write_blocker.py +21 -7
- package/hooks/blocking/verified_commit_gate.py +11 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +16 -1
- package/hooks/blocking/windows_rmtree_blocker.py +7 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +10 -5
- package/hooks/blocking/write_existing_file_blocker.py +16 -1
- package/hooks/hooks.json +19 -79
- package/hooks/hooks_constants/CLAUDE.md +7 -1
- package/hooks/hooks_constants/blocking_check_limits.py +74 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +9 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +45 -0
- package/hooks/hooks_constants/dead_config_field_constants.py +5 -5
- package/hooks/hooks_constants/docstring_rule_gate_count_blocker_constants.py +90 -0
- package/hooks/hooks_constants/hook_block_logger.py +59 -0
- package/hooks/hooks_constants/multi_edit_reconstruction.py +56 -0
- package/hooks/hooks_constants/mypy_validator_cache_constants.py +36 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +111 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +68 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +143 -0
- package/hooks/hooks_constants/pytest_testpaths_orphan_blocker_constants.py +79 -0
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +18 -0
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +44 -0
- package/hooks/hooks_constants/test_hook_block_logger.py +159 -0
- package/hooks/lifecycle/config_change_guard.py +12 -0
- package/hooks/lifecycle/test_config_change_guard.py +23 -0
- package/hooks/validation/hook_format_validator.py +13 -0
- package/hooks/validation/mypy_validator.py +245 -18
- package/hooks/validation/post_tool_use_dispatcher.py +344 -0
- package/hooks/validation/test_hook_format_validator.py +64 -0
- package/hooks/validation/test_mypy_validator.py +206 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +610 -0
- package/hooks/workflow/test_auto_formatter.py +10 -9
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +4 -2
- package/rules/package-inventory-stale-entry.md +24 -0
- package/skills/autoconverge/SKILL.md +111 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +106 -0
- package/skills/autoconverge/workflow/converge.mjs +29 -3
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +47 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +161 -0
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +100 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Tests for check_docstring_unguarded_malformed_payload_claim — Category O6 drift.
|
|
2
|
+
|
|
3
|
+
A function docstring that promises "a malformed payload resolves to None" asserts
|
|
4
|
+
the body catches a bad payload and turns it into a None return. The claim drifts
|
|
5
|
+
when the value construction that dereferences payload fields (``payload["key"]``,
|
|
6
|
+
``float(payload["key"])``) sits OUTSIDE the try/except whose handler returns None:
|
|
7
|
+
a present-but-malformed payload raises KeyError or TypeError from that unguarded
|
|
8
|
+
dereference and propagates rather than resolving to None. This is the deterministic
|
|
9
|
+
slice of Category O6 (docstring prose vs implementation drift) for an
|
|
10
|
+
exception-guard claim.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib.util
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_enforcer_module() -> ModuleType:
|
|
21
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
22
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
23
|
+
assert spec is not None
|
|
24
|
+
assert spec.loader is not None
|
|
25
|
+
module = importlib.util.module_from_spec(spec)
|
|
26
|
+
spec.loader.exec_module(module)
|
|
27
|
+
return module
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_docstring_unguarded_malformed_payload_claim(content: str, file_path: str) -> list[str]:
|
|
34
|
+
return code_rules_enforcer.check_docstring_unguarded_malformed_payload_claim(content, file_path)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
PRODUCTION_FILE_PATH = "/project/shared/human_actions.py"
|
|
38
|
+
TEST_FILE_PATH = "/project/shared/test_human_actions.py"
|
|
39
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_UNGUARDED_DEREFERENCE_BODY = (
|
|
43
|
+
"def read_metrics(self, selector: str) -> object:\n"
|
|
44
|
+
' """Read the container geometry over CDP, or None on failure.\n'
|
|
45
|
+
"\n"
|
|
46
|
+
" A missing element, a CDP error, or a malformed payload resolves to None\n"
|
|
47
|
+
" so the caller skips the drag.\n"
|
|
48
|
+
' """\n'
|
|
49
|
+
" try:\n"
|
|
50
|
+
" evaluate_payload = self.cdp.run(selector)\n"
|
|
51
|
+
" parsed_metrics = json.loads(evaluate_payload)\n"
|
|
52
|
+
" except (KeyError, TypeError, ValueError):\n"
|
|
53
|
+
" return None\n"
|
|
54
|
+
' if not parsed_metrics.get("found"):\n'
|
|
55
|
+
" return None\n"
|
|
56
|
+
" return Metrics(\n"
|
|
57
|
+
' client_width=float(parsed_metrics["client_width"]),\n'
|
|
58
|
+
' client_height=float(parsed_metrics["client_height"]),\n'
|
|
59
|
+
" )\n"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_flags_dereference_outside_the_guarded_block() -> None:
|
|
64
|
+
issues = check_docstring_unguarded_malformed_payload_claim(
|
|
65
|
+
_UNGUARDED_DEREFERENCE_BODY, PRODUCTION_FILE_PATH
|
|
66
|
+
)
|
|
67
|
+
assert len(issues) == 1
|
|
68
|
+
assert "read_metrics" in issues[0]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_passes_when_dereference_sits_inside_the_guarded_block() -> None:
|
|
72
|
+
content = (
|
|
73
|
+
"def read_metrics(self, selector: str) -> object:\n"
|
|
74
|
+
' """Read the container geometry over CDP, or None on failure.\n'
|
|
75
|
+
"\n"
|
|
76
|
+
" A missing element, a CDP error, or a malformed payload resolves to None\n"
|
|
77
|
+
" so the caller skips the drag.\n"
|
|
78
|
+
' """\n'
|
|
79
|
+
" try:\n"
|
|
80
|
+
" evaluate_payload = self.cdp.run(selector)\n"
|
|
81
|
+
" parsed_metrics = json.loads(evaluate_payload)\n"
|
|
82
|
+
' if not parsed_metrics.get("found"):\n'
|
|
83
|
+
" return None\n"
|
|
84
|
+
" return Metrics(\n"
|
|
85
|
+
' client_width=float(parsed_metrics["client_width"]),\n'
|
|
86
|
+
' client_height=float(parsed_metrics["client_height"]),\n'
|
|
87
|
+
" )\n"
|
|
88
|
+
" except (KeyError, TypeError, ValueError):\n"
|
|
89
|
+
" return None\n"
|
|
90
|
+
)
|
|
91
|
+
assert check_docstring_unguarded_malformed_payload_claim(content, PRODUCTION_FILE_PATH) == []
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_passes_when_docstring_makes_no_malformed_payload_claim() -> None:
|
|
95
|
+
content = (
|
|
96
|
+
"def read_metrics(self, selector: str) -> object:\n"
|
|
97
|
+
' """Read the container geometry over CDP, or None on failure."""\n'
|
|
98
|
+
" try:\n"
|
|
99
|
+
" parsed_metrics = json.loads(self.cdp.run(selector))\n"
|
|
100
|
+
" except (KeyError, TypeError, ValueError):\n"
|
|
101
|
+
" return None\n"
|
|
102
|
+
" return Metrics(\n"
|
|
103
|
+
' client_width=float(parsed_metrics["client_width"]),\n'
|
|
104
|
+
" )\n"
|
|
105
|
+
)
|
|
106
|
+
assert check_docstring_unguarded_malformed_payload_claim(content, PRODUCTION_FILE_PATH) == []
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_passes_when_no_subscript_dereference_follows_the_guard() -> None:
|
|
110
|
+
content = (
|
|
111
|
+
"def read_metrics(self, selector: str) -> object:\n"
|
|
112
|
+
' """Read the geometry over CDP, or None on failure.\n'
|
|
113
|
+
"\n"
|
|
114
|
+
" A malformed payload resolves to None so the caller skips the drag.\n"
|
|
115
|
+
' """\n'
|
|
116
|
+
" try:\n"
|
|
117
|
+
" parsed_metrics = json.loads(self.cdp.run(selector))\n"
|
|
118
|
+
" except (KeyError, TypeError, ValueError):\n"
|
|
119
|
+
" return None\n"
|
|
120
|
+
' return parsed_metrics.get("found")\n'
|
|
121
|
+
)
|
|
122
|
+
assert check_docstring_unguarded_malformed_payload_claim(content, PRODUCTION_FILE_PATH) == []
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_passes_when_post_guard_subscript_dereferences_an_unrelated_name() -> None:
|
|
126
|
+
content = (
|
|
127
|
+
"def read_metrics(self, selector: str) -> object:\n"
|
|
128
|
+
' """Read the geometry over CDP, or None on failure.\n'
|
|
129
|
+
"\n"
|
|
130
|
+
" A malformed payload resolves to None so the caller skips the drag.\n"
|
|
131
|
+
' """\n'
|
|
132
|
+
" try:\n"
|
|
133
|
+
" parsed_metrics = json.loads(self.cdp.run(selector))\n"
|
|
134
|
+
' width = float(parsed_metrics["client_width"])\n'
|
|
135
|
+
" except (KeyError, TypeError, ValueError):\n"
|
|
136
|
+
" return None\n"
|
|
137
|
+
" fallback = cache[selector]\n"
|
|
138
|
+
" return Metrics(client_width=width, fallback=fallback)\n"
|
|
139
|
+
)
|
|
140
|
+
assert check_docstring_unguarded_malformed_payload_claim(content, PRODUCTION_FILE_PATH) == []
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _unguarded_body_with_claim(claim_sentence: str) -> str:
|
|
144
|
+
return (
|
|
145
|
+
"def read_metrics(self, selector: str) -> object:\n"
|
|
146
|
+
' """Read the container geometry over CDP, or None on failure.\n'
|
|
147
|
+
"\n"
|
|
148
|
+
f" {claim_sentence}\n"
|
|
149
|
+
' """\n'
|
|
150
|
+
" try:\n"
|
|
151
|
+
" parsed_metrics = json.loads(self.cdp.run(selector))\n"
|
|
152
|
+
" except (KeyError, TypeError, ValueError):\n"
|
|
153
|
+
" return None\n"
|
|
154
|
+
' return float(parsed_metrics["client_width"])\n'
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_article_prefixed_claim_phrasings_still_flag_via_articleless_substring() -> None:
|
|
159
|
+
all_article_prefixed_claims = (
|
|
160
|
+
"A malformed payload resolves to None so the caller skips the drag.",
|
|
161
|
+
"A bad payload resolves to None so the caller skips the drag.",
|
|
162
|
+
"An invalid payload resolves to None so the caller skips the drag.",
|
|
163
|
+
"A malformed payload yields None so the caller skips the drag.",
|
|
164
|
+
)
|
|
165
|
+
for each_claim in all_article_prefixed_claims:
|
|
166
|
+
issues = check_docstring_unguarded_malformed_payload_claim(
|
|
167
|
+
_unguarded_body_with_claim(each_claim), PRODUCTION_FILE_PATH
|
|
168
|
+
)
|
|
169
|
+
assert len(issues) == 1
|
|
170
|
+
assert "read_metrics" in issues[0]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_test_files_are_exempt() -> None:
|
|
174
|
+
assert (
|
|
175
|
+
check_docstring_unguarded_malformed_payload_claim(
|
|
176
|
+
_UNGUARDED_DEREFERENCE_BODY, TEST_FILE_PATH
|
|
177
|
+
)
|
|
178
|
+
== []
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_hook_infrastructure_is_exempt() -> None:
|
|
183
|
+
assert (
|
|
184
|
+
check_docstring_unguarded_malformed_payload_claim(
|
|
185
|
+
_UNGUARDED_DEREFERENCE_BODY, HOOK_INFRASTRUCTURE_PATH
|
|
186
|
+
)
|
|
187
|
+
== []
|
|
188
|
+
)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Tests for the module-docstring check roster and docstring tuple-enumeration checks.
|
|
2
|
+
|
|
3
|
+
Both checks catch docstring-prose-vs-implementation drift in a check-registry
|
|
4
|
+
module — a hook module that exposes several public ``check_*`` functions and a
|
|
5
|
+
module-level tuple of literal attribute names. The drift the
|
|
6
|
+
``code_rules_test_assertions.py`` module hit at PR #713 HEAD: a one-line module
|
|
7
|
+
docstring that names four of its five public checks, and a function docstring
|
|
8
|
+
that enumerates three plumbing attributes while the tuple it reads holds four.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_enforcer_module() -> ModuleType:
|
|
19
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
20
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
21
|
+
assert spec is not None
|
|
22
|
+
assert spec.loader is not None
|
|
23
|
+
module = importlib.util.module_from_spec(spec)
|
|
24
|
+
spec.loader.exec_module(module)
|
|
25
|
+
return module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_module_docstring_names_public_checks(content: str, file_path: str) -> list[str]:
|
|
32
|
+
return code_rules_enforcer.check_module_docstring_names_public_checks(content, file_path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_docstring_tuple_enumeration_match(content: str, file_path: str) -> list[str]:
|
|
36
|
+
return code_rules_enforcer.check_docstring_tuple_enumeration_match(content, file_path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
40
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/code_rules_test_assertions.py"
|
|
44
|
+
PRODUCTION_FILE_PATH = "/project/src/registry.py"
|
|
45
|
+
TEST_FILE_PATH = "/project/src/test_registry.py"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _registry_module_omitting_a_check() -> str:
|
|
49
|
+
return (
|
|
50
|
+
'"""Skip-decorator, existence-only, and constant-equality test-quality checks."""\n'
|
|
51
|
+
"\n"
|
|
52
|
+
"def check_skip_decorators(content: str, file_path: str) -> list[str]:\n"
|
|
53
|
+
" return []\n"
|
|
54
|
+
"\n"
|
|
55
|
+
"def check_existence_check(content: str, file_path: str) -> list[str]:\n"
|
|
56
|
+
" return []\n"
|
|
57
|
+
"\n"
|
|
58
|
+
"def check_constant_equality(content: str, file_path: str) -> list[str]:\n"
|
|
59
|
+
" return []\n"
|
|
60
|
+
"\n"
|
|
61
|
+
"def check_behavior_named_mock(content: str, file_path: str) -> list[str]:\n"
|
|
62
|
+
" return []\n"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_should_flag_module_docstring_omitting_a_public_check() -> None:
|
|
67
|
+
issues = check_module_docstring_names_public_checks(
|
|
68
|
+
_registry_module_omitting_a_check(), HOOK_INFRASTRUCTURE_PATH
|
|
69
|
+
)
|
|
70
|
+
assert any("check_behavior_named_mock" in each for each in issues), (
|
|
71
|
+
f"Expected the omitted check to flag, got: {issues!r}"
|
|
72
|
+
)
|
|
73
|
+
assert len(issues) == 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_should_not_flag_module_docstring_naming_every_public_check() -> None:
|
|
77
|
+
source = (
|
|
78
|
+
'"""Skip-decorator, existence-check, constant-equality, and behavior-named-mock checks."""\n'
|
|
79
|
+
"\n"
|
|
80
|
+
"def check_skip_decorators(content: str, file_path: str) -> list[str]:\n"
|
|
81
|
+
" return []\n"
|
|
82
|
+
"\n"
|
|
83
|
+
"def check_existence_check(content: str, file_path: str) -> list[str]:\n"
|
|
84
|
+
" return []\n"
|
|
85
|
+
"\n"
|
|
86
|
+
"def check_constant_equality(content: str, file_path: str) -> list[str]:\n"
|
|
87
|
+
" return []\n"
|
|
88
|
+
"\n"
|
|
89
|
+
"def check_behavior_named_mock(content: str, file_path: str) -> list[str]:\n"
|
|
90
|
+
" return []\n"
|
|
91
|
+
)
|
|
92
|
+
issues = check_module_docstring_names_public_checks(source, HOOK_INFRASTRUCTURE_PATH)
|
|
93
|
+
assert issues == [], f"Docstring naming every check must not flag, got: {issues!r}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_should_not_flag_module_with_a_single_public_check() -> None:
|
|
97
|
+
source = (
|
|
98
|
+
'"""Skip-decorator test-quality check."""\n'
|
|
99
|
+
"\n"
|
|
100
|
+
"def check_skip_decorators(content: str, file_path: str) -> list[str]:\n"
|
|
101
|
+
" return []\n"
|
|
102
|
+
)
|
|
103
|
+
issues = check_module_docstring_names_public_checks(source, HOOK_INFRASTRUCTURE_PATH)
|
|
104
|
+
assert issues == [], f"A one-check module must not flag, got: {issues!r}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_should_not_flag_multi_paragraph_module_docstring() -> None:
|
|
108
|
+
source = (
|
|
109
|
+
'"""Skip-decorator and existence-check test-quality checks.\n'
|
|
110
|
+
"\n"
|
|
111
|
+
" The roster grows over time; the audit lane reads the full prose body.\n"
|
|
112
|
+
' """\n'
|
|
113
|
+
"\n"
|
|
114
|
+
"def check_skip_decorators(content: str, file_path: str) -> list[str]:\n"
|
|
115
|
+
" return []\n"
|
|
116
|
+
"\n"
|
|
117
|
+
"def check_behavior_named_mock(content: str, file_path: str) -> list[str]:\n"
|
|
118
|
+
" return []\n"
|
|
119
|
+
)
|
|
120
|
+
issues = check_module_docstring_names_public_checks(source, HOOK_INFRASTRUCTURE_PATH)
|
|
121
|
+
assert issues == [], f"Multi-paragraph docstrings go to the audit lane, got: {issues!r}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_should_skip_module_without_docstring() -> None:
|
|
125
|
+
source = (
|
|
126
|
+
"def check_skip_decorators(content: str, file_path: str) -> list[str]:\n"
|
|
127
|
+
" return []\n"
|
|
128
|
+
"\n"
|
|
129
|
+
"def check_behavior_named_mock(content: str, file_path: str) -> list[str]:\n"
|
|
130
|
+
" return []\n"
|
|
131
|
+
)
|
|
132
|
+
issues = check_module_docstring_names_public_checks(source, HOOK_INFRASTRUCTURE_PATH)
|
|
133
|
+
assert issues == [], f"No-docstring modules are out of scope, got: {issues!r}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_should_skip_private_check_helpers() -> None:
|
|
137
|
+
source = (
|
|
138
|
+
'"""Skip-decorator and existence-check test-quality checks."""\n'
|
|
139
|
+
"\n"
|
|
140
|
+
"def check_skip_decorators(content: str, file_path: str) -> list[str]:\n"
|
|
141
|
+
" return []\n"
|
|
142
|
+
"\n"
|
|
143
|
+
"def check_existence_check(content: str, file_path: str) -> list[str]:\n"
|
|
144
|
+
" return []\n"
|
|
145
|
+
"\n"
|
|
146
|
+
"def _check_internal_helper(content: str) -> bool:\n"
|
|
147
|
+
" return False\n"
|
|
148
|
+
)
|
|
149
|
+
issues = check_module_docstring_names_public_checks(source, HOOK_INFRASTRUCTURE_PATH)
|
|
150
|
+
assert issues == [], f"Private check helpers are not roster surface, got: {issues!r}"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_should_skip_test_file_for_module_roster() -> None:
|
|
154
|
+
issues = check_module_docstring_names_public_checks(
|
|
155
|
+
_registry_module_omitting_a_check(), TEST_FILE_PATH
|
|
156
|
+
)
|
|
157
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_should_handle_module_roster_syntax_error_gracefully() -> None:
|
|
161
|
+
issues = check_module_docstring_names_public_checks("def broken(\n", HOOK_INFRASTRUCTURE_PATH)
|
|
162
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _module_with_drifted_tuple_enumeration() -> str:
|
|
166
|
+
return (
|
|
167
|
+
'"""Call-args plumbing detection."""\n'
|
|
168
|
+
"\n"
|
|
169
|
+
'_CALL_ARGS_PLUMBING_ATTRIBUTES = ("call_args", "call_args_list", "called", "call_count")\n'
|
|
170
|
+
"\n"
|
|
171
|
+
"def check_plumbing(content: str, file_path: str) -> list[str]:\n"
|
|
172
|
+
' """Advise when a test asserts only on call-args plumbing.\n'
|
|
173
|
+
"\n"
|
|
174
|
+
" A body asserting on call-args plumbing (``call_args``, ``call_args_list``,\n"
|
|
175
|
+
" ``.kwargs``) reaches into mock internals rather than observed behavior.\n"
|
|
176
|
+
' """\n'
|
|
177
|
+
" for each_name in _CALL_ARGS_PLUMBING_ATTRIBUTES:\n"
|
|
178
|
+
" if each_name in content:\n"
|
|
179
|
+
" return [each_name]\n"
|
|
180
|
+
" return []\n"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_should_flag_docstring_tuple_enumeration_drift() -> None:
|
|
185
|
+
issues = check_docstring_tuple_enumeration_match(
|
|
186
|
+
_module_with_drifted_tuple_enumeration(), HOOK_INFRASTRUCTURE_PATH
|
|
187
|
+
)
|
|
188
|
+
assert any("check_plumbing" in each for each in issues), (
|
|
189
|
+
f"Expected the drifted enumeration to flag, got: {issues!r}"
|
|
190
|
+
)
|
|
191
|
+
assert any("kwargs" in each for each in issues), (
|
|
192
|
+
f"Expected the docstring-only token named in the message, got: {issues!r}"
|
|
193
|
+
)
|
|
194
|
+
assert any("called" in each for each in issues), (
|
|
195
|
+
f"Expected a tuple-only member named in the message, got: {issues!r}"
|
|
196
|
+
)
|
|
197
|
+
assert len(issues) == 1
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_should_not_flag_docstring_tuple_enumeration_match() -> None:
|
|
201
|
+
source = (
|
|
202
|
+
'"""Call-args plumbing detection."""\n'
|
|
203
|
+
"\n"
|
|
204
|
+
'_CALL_ARGS_PLUMBING_ATTRIBUTES = ("call_args", "call_args_list", "called", "call_count")\n'
|
|
205
|
+
"\n"
|
|
206
|
+
"def check_plumbing(content: str, file_path: str) -> list[str]:\n"
|
|
207
|
+
' """Advise when a test asserts only on call-args plumbing.\n'
|
|
208
|
+
"\n"
|
|
209
|
+
" A body asserting on call-args plumbing (``call_args``, ``call_args_list``,\n"
|
|
210
|
+
" ``called``, ``call_count``) reaches into mock internals.\n"
|
|
211
|
+
' """\n'
|
|
212
|
+
" for each_name in _CALL_ARGS_PLUMBING_ATTRIBUTES:\n"
|
|
213
|
+
" if each_name in content:\n"
|
|
214
|
+
" return [each_name]\n"
|
|
215
|
+
" return []\n"
|
|
216
|
+
)
|
|
217
|
+
issues = check_docstring_tuple_enumeration_match(source, HOOK_INFRASTRUCTURE_PATH)
|
|
218
|
+
assert issues == [], f"Matching enumeration must not flag, got: {issues!r}"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_should_not_flag_docstring_without_tuple_overlap() -> None:
|
|
222
|
+
source = (
|
|
223
|
+
'"""Call-args plumbing detection."""\n'
|
|
224
|
+
"\n"
|
|
225
|
+
'_CALL_ARGS_PLUMBING_ATTRIBUTES = ("call_args", "call_args_list", "called", "call_count")\n'
|
|
226
|
+
"\n"
|
|
227
|
+
"def check_plumbing(content: str, file_path: str) -> list[str]:\n"
|
|
228
|
+
' """Advise when a test asserts only on mock internals.\n'
|
|
229
|
+
"\n"
|
|
230
|
+
" A body reading ``foo``, ``bar``, ``baz`` is unrelated to the tuple.\n"
|
|
231
|
+
' """\n'
|
|
232
|
+
" for each_name in _CALL_ARGS_PLUMBING_ATTRIBUTES:\n"
|
|
233
|
+
" if each_name in content:\n"
|
|
234
|
+
" return [each_name]\n"
|
|
235
|
+
" return []\n"
|
|
236
|
+
)
|
|
237
|
+
issues = check_docstring_tuple_enumeration_match(source, HOOK_INFRASTRUCTURE_PATH)
|
|
238
|
+
assert issues == [], f"A docstring not naming the tuple must not flag, got: {issues!r}"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_should_not_flag_when_function_does_not_reference_the_tuple() -> None:
|
|
242
|
+
source = (
|
|
243
|
+
'"""Call-args plumbing detection."""\n'
|
|
244
|
+
"\n"
|
|
245
|
+
'_CALL_ARGS_PLUMBING_ATTRIBUTES = ("call_args", "call_args_list", "called", "call_count")\n'
|
|
246
|
+
"\n"
|
|
247
|
+
"def check_unrelated(content: str, file_path: str) -> list[str]:\n"
|
|
248
|
+
' """Advise on ``call_args`` and ``call_args_list`` usage in tests."""\n'
|
|
249
|
+
" return [content[:0]]\n"
|
|
250
|
+
)
|
|
251
|
+
issues = check_docstring_tuple_enumeration_match(source, HOOK_INFRASTRUCTURE_PATH)
|
|
252
|
+
assert issues == [], f"A function not reading the tuple must not flag, got: {issues!r}"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_should_handle_tuple_enumeration_syntax_error_gracefully() -> None:
|
|
256
|
+
issues = check_docstring_tuple_enumeration_match("def broken(\n", HOOK_INFRASTRUCTURE_PATH)
|
|
257
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_validate_content_surfaces_module_roster_drift() -> None:
|
|
261
|
+
issues = validate_content(
|
|
262
|
+
_registry_module_omitting_a_check(), HOOK_INFRASTRUCTURE_PATH, old_content=""
|
|
263
|
+
)
|
|
264
|
+
matching_issues = [
|
|
265
|
+
each for each in issues if "check_behavior_named_mock" in each and "docstring" in each
|
|
266
|
+
]
|
|
267
|
+
assert matching_issues, (
|
|
268
|
+
f"Expected validate_content to surface the module-roster drift, got: {issues!r}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_validate_content_surfaces_tuple_enumeration_drift() -> None:
|
|
273
|
+
issues = validate_content(
|
|
274
|
+
_module_with_drifted_tuple_enumeration(), HOOK_INFRASTRUCTURE_PATH, old_content=""
|
|
275
|
+
)
|
|
276
|
+
matching_issues = [each for each in issues if "check_plumbing" in each and "enumerat" in each]
|
|
277
|
+
assert matching_issues, (
|
|
278
|
+
f"Expected validate_content to surface the tuple-enumeration drift, got: {issues!r}"
|
|
279
|
+
)
|