claude-dev-env 2.28.0 → 2.28.1
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/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/docs/CODE_RULES.md +3 -3
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/references/prose-style-enforcement.md +5 -9
- package/hooks/blocking/AGENTS.md +3 -13
- package/hooks/blocking/code_rules_docstrings.py +10 -2138
- package/hooks/blocking/code_rules_enforcer.py +0 -121
- package/hooks/blocking/code_rules_imports_logging.py +1 -236
- package/hooks/blocking/code_rules_shared.py +23 -0
- package/hooks/blocking/code_rules_test_layout.py +8 -8
- package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
- package/hooks/blocking/precommit_code_rules_gate.py +8 -43
- package/hooks/blocking/state_description_blocker.py +1 -7
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
- package/hooks/blocking/test_code_rules_shared.py +12 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
- package/hooks/blocking/test_state_description_blocker.py +6 -4
- package/hooks/blocking/test_stop_dispatcher.py +5 -29
- package/hooks/hooks_constants/AGENTS.md +1 -6
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/messages.py +0 -2
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/test_messages.py +5 -3
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
- package/package.json +1 -1
- package/rules/AGENTS.md +3 -6
- package/rules/claims-as-quotes.md +0 -10
- package/rules/code-standards.md +6 -6
- package/rules/explore-thoroughly.md +0 -1
- package/rules/failure-blast-radius.md +0 -8
- package/rules/falsify-before-green.md +0 -8
- package/rules/file-global-constants.md +2 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +1 -9
- package/rules/hedging-claims.md +2 -6
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/measurement-denominators.md +0 -9
- package/rules/research-mode.md +0 -6
- package/rules/verify-before-asking.md +0 -5
- package/rules/verify-runtime-state.md +0 -5
- package/scripts/codex_compat_materializer.py +0 -12
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/tests/test_engine.py +0 -1
- package/scripts/tests/test_rules.py +0 -1
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/system-prompts/software-engineer.xml +3 -3
- package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
- package/hooks/blocking/code_rules_dead_config_field.py +0 -568
- package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
- package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
- package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
- package/hooks/blocking/code_rules_mock_completeness.py +0 -295
- package/hooks/blocking/code_rules_scope_binding.py +0 -151
- package/hooks/blocking/code_rules_unused_imports.py +0 -197
- package/hooks/blocking/hedging_language_blocker.py +0 -221
- package/hooks/blocking/intent_only_ending_blocker.py +0 -148
- package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
- package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
- package/hooks/blocking/test_hedging_language_blocker.py +0 -261
- package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
- package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
- package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
- package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
- package/rules/conservative-action.md +0 -17
- package/rules/context7.md +0 -8
- package/rules/few-words.md +0 -3
- package/rules/parallel-tools.md +0 -23
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"""Tests for the ruff-I001 import-block-sort gate in code_rules_imports_logging.
|
|
2
|
-
|
|
3
|
-
The gate delegates to ruff itself, so these tests run real ruff over real source
|
|
4
|
-
shapes (the same way the repo quality gate does) rather than mocking the
|
|
5
|
-
subprocess. A sorted block passes; the PR #749 shape — two local ``from`` imports
|
|
6
|
-
out of alphabetical order in one contiguous block — fails with an I001 issue.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import importlib.util
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from types import ModuleType
|
|
12
|
-
|
|
13
|
-
IMPORTS_MODULE_FILENAME = "code_rules_imports_logging.py"
|
|
14
|
-
IMPORTS_MODULE_NAME = "code_rules_imports_logging_under_test"
|
|
15
|
-
PRODUCTION_PYTHON_TARGET = str(Path(__file__).parent / "production_module.py")
|
|
16
|
-
|
|
17
|
-
SORTED_IMPORT_BLOCK_SOURCE = (
|
|
18
|
-
"import json\n"
|
|
19
|
-
"import os\n"
|
|
20
|
-
"\n"
|
|
21
|
-
"from package.alpha import first\n"
|
|
22
|
-
"from package.beta import second\n"
|
|
23
|
-
"from package.gamma import third\n"
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
UNSORTED_IMPORT_BLOCK_SOURCE = (
|
|
27
|
-
"import json\n"
|
|
28
|
-
"import os\n"
|
|
29
|
-
"\n"
|
|
30
|
-
"from package.gamma import third\n"
|
|
31
|
-
"from package.alpha import first\n"
|
|
32
|
-
"from package.beta import second\n"
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def load_imports_module() -> ModuleType:
|
|
37
|
-
module_path = Path(__file__).parent / IMPORTS_MODULE_FILENAME
|
|
38
|
-
module_spec = importlib.util.spec_from_file_location(IMPORTS_MODULE_NAME, module_path)
|
|
39
|
-
assert module_spec is not None
|
|
40
|
-
assert module_spec.loader is not None
|
|
41
|
-
imports_module = importlib.util.module_from_spec(module_spec)
|
|
42
|
-
module_spec.loader.exec_module(imports_module)
|
|
43
|
-
return imports_module
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
imports_logging = load_imports_module()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_should_flag_unsorted_import_block() -> None:
|
|
50
|
-
issues = imports_logging.check_import_block_sorted(
|
|
51
|
-
UNSORTED_IMPORT_BLOCK_SOURCE, PRODUCTION_PYTHON_TARGET
|
|
52
|
-
)
|
|
53
|
-
assert len(issues) == 1
|
|
54
|
-
assert "un-sorted (ruff I001)" in issues[0]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_should_allow_sorted_import_block() -> None:
|
|
58
|
-
issues = imports_logging.check_import_block_sorted(
|
|
59
|
-
SORTED_IMPORT_BLOCK_SOURCE, PRODUCTION_PYTHON_TARGET
|
|
60
|
-
)
|
|
61
|
-
assert issues == []
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_should_report_the_block_anchor_line_number() -> None:
|
|
65
|
-
issues = imports_logging.check_import_block_sorted(
|
|
66
|
-
UNSORTED_IMPORT_BLOCK_SOURCE, PRODUCTION_PYTHON_TARGET
|
|
67
|
-
)
|
|
68
|
-
assert issues[0].startswith("Line 1:")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_should_exempt_test_files() -> None:
|
|
72
|
-
issues = imports_logging.check_import_block_sorted(
|
|
73
|
-
UNSORTED_IMPORT_BLOCK_SOURCE, "test_something.py"
|
|
74
|
-
)
|
|
75
|
-
assert issues == []
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_should_exempt_non_python_files() -> None:
|
|
79
|
-
issues = imports_logging.check_import_block_sorted(UNSORTED_IMPORT_BLOCK_SOURCE, "notes.md")
|
|
80
|
-
assert issues == []
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_should_fail_open_on_unparseable_source() -> None:
|
|
84
|
-
broken_source = "def (:::\n import os\n"
|
|
85
|
-
issues = imports_logging.check_import_block_sorted(broken_source, PRODUCTION_PYTHON_TARGET)
|
|
86
|
-
assert issues == []
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def test_should_fail_open_when_no_ruff_config_is_discoverable(tmp_path: Path) -> None:
|
|
90
|
-
target_without_config = tmp_path / "standalone_module.py"
|
|
91
|
-
issues = imports_logging.check_import_block_sorted(
|
|
92
|
-
UNSORTED_IMPORT_BLOCK_SOURCE, str(target_without_config)
|
|
93
|
-
)
|
|
94
|
-
assert issues == []
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_should_skip_unsorted_block_when_edit_is_distant_from_it() -> None:
|
|
98
|
-
changed_lines_away_from_block = {9}
|
|
99
|
-
issues = imports_logging.check_import_block_sorted(
|
|
100
|
-
UNSORTED_IMPORT_BLOCK_SOURCE,
|
|
101
|
-
PRODUCTION_PYTHON_TARGET,
|
|
102
|
-
changed_lines_away_from_block,
|
|
103
|
-
)
|
|
104
|
-
assert issues == []
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def test_should_flag_unsorted_block_when_edit_touches_anchor_line() -> None:
|
|
108
|
-
changed_lines_on_block_anchor = {1}
|
|
109
|
-
issues = imports_logging.check_import_block_sorted(
|
|
110
|
-
UNSORTED_IMPORT_BLOCK_SOURCE,
|
|
111
|
-
PRODUCTION_PYTHON_TARGET,
|
|
112
|
-
changed_lines_on_block_anchor,
|
|
113
|
-
)
|
|
114
|
-
assert len(issues) == 1
|
|
115
|
-
assert "un-sorted (ruff I001)" in issues[0]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_should_flag_unsorted_block_when_edit_touches_non_anchor_block_line() -> None:
|
|
119
|
-
changed_lines_inside_block_below_anchor = {4}
|
|
120
|
-
issues = imports_logging.check_import_block_sorted(
|
|
121
|
-
UNSORTED_IMPORT_BLOCK_SOURCE,
|
|
122
|
-
PRODUCTION_PYTHON_TARGET,
|
|
123
|
-
changed_lines_inside_block_below_anchor,
|
|
124
|
-
)
|
|
125
|
-
assert len(issues) == 1
|
|
126
|
-
assert "un-sorted (ruff I001)" in issues[0]
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def test_should_flag_unsorted_block_when_edit_touches_last_block_line() -> None:
|
|
130
|
-
changed_lines_on_block_final_line = {6}
|
|
131
|
-
issues = imports_logging.check_import_block_sorted(
|
|
132
|
-
UNSORTED_IMPORT_BLOCK_SOURCE,
|
|
133
|
-
PRODUCTION_PYTHON_TARGET,
|
|
134
|
-
changed_lines_on_block_final_line,
|
|
135
|
-
)
|
|
136
|
-
assert len(issues) == 1
|
|
137
|
-
assert "un-sorted (ruff I001)" in issues[0]
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def test_should_flag_unsorted_block_when_changed_lines_is_none() -> None:
|
|
141
|
-
issues = imports_logging.check_import_block_sorted(
|
|
142
|
-
UNSORTED_IMPORT_BLOCK_SOURCE,
|
|
143
|
-
PRODUCTION_PYTHON_TARGET,
|
|
144
|
-
None,
|
|
145
|
-
)
|
|
146
|
-
assert len(issues) == 1
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def test_should_defer_unsorted_block_to_caller_scope() -> None:
|
|
150
|
-
changed_lines_away_from_block = {9}
|
|
151
|
-
issues = imports_logging.check_import_block_sorted(
|
|
152
|
-
UNSORTED_IMPORT_BLOCK_SOURCE,
|
|
153
|
-
PRODUCTION_PYTHON_TARGET,
|
|
154
|
-
changed_lines_away_from_block,
|
|
155
|
-
defer_scope_to_caller=True,
|
|
156
|
-
)
|
|
157
|
-
assert len(issues) == 1
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
"""Behavior tests for the code_rules_mock_completeness code-rules check module."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from types import SimpleNamespace
|
|
8
|
-
|
|
9
|
-
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
-
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
-
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
-
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
-
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
-
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
-
|
|
16
|
-
from code_rules_mock_completeness import ( # noqa: E402
|
|
17
|
-
check_incomplete_mocks,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
code_rules_enforcer = SimpleNamespace(
|
|
21
|
-
check_incomplete_mocks=check_incomplete_mocks,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
INCOMPLETE_MOCK_PRODUCTION_FILE_PATH = "packages/app/services/orders.py"
|
|
26
|
-
|
|
27
|
-
INCOMPLETE_MOCK_TEST_FILE_PATH = "packages/app/tests/test_orders.py"
|
|
28
|
-
|
|
29
|
-
MODULE_LEVEL_MOCK_TEST_FILE_PATH = "packages/app/tests/test_module_level.py"
|
|
30
|
-
|
|
31
|
-
SCOPE_KEYED_MOCK_TEST_FILE_PATH = "packages/app/tests/test_scope_mocks.py"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _assert_inner_field_did_not_leak(
|
|
35
|
-
captured_stderr: str,
|
|
36
|
-
inner_only_field_name: str,
|
|
37
|
-
binding_form_description: str,
|
|
38
|
-
) -> None:
|
|
39
|
-
leaked_advisories = [
|
|
40
|
-
line
|
|
41
|
-
for line in captured_stderr.splitlines()
|
|
42
|
-
if "mock_user" in line and inner_only_field_name in line
|
|
43
|
-
]
|
|
44
|
-
assert leaked_advisories == [], (
|
|
45
|
-
f"Expected no advisory on the outer mock for {inner_only_field_name!r} — "
|
|
46
|
-
f"that field is accessed only inside a nested scope that re-binds "
|
|
47
|
-
f"mock_user via {binding_form_description}, got: {captured_stderr!r}"
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_should_advise_when_mock_missing_accessed_field(capsys: object) -> None:
|
|
52
|
-
source = (
|
|
53
|
-
"mock_order = {'id': 1}\n"
|
|
54
|
-
"\n"
|
|
55
|
-
"def test_order_total() -> None:\n"
|
|
56
|
-
" total = mock_order['total']\n"
|
|
57
|
-
" assert total > 0\n"
|
|
58
|
-
)
|
|
59
|
-
code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
|
|
60
|
-
captured = getattr(capsys, "readouterr")()
|
|
61
|
-
assert "mock_order" in captured.err and "total" in captured.err, (
|
|
62
|
-
f"Expected advisory about missing 'total' field, got: {captured.err!r}"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_should_not_advise_when_mock_has_all_accessed_fields(capsys: object) -> None:
|
|
67
|
-
source = (
|
|
68
|
-
"mock_order = {'id': 1, 'total': 50}\n"
|
|
69
|
-
"\n"
|
|
70
|
-
"def test_order_total() -> None:\n"
|
|
71
|
-
" total = mock_order['total']\n"
|
|
72
|
-
" assert total > 0\n"
|
|
73
|
-
)
|
|
74
|
-
code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
|
|
75
|
-
captured = getattr(capsys, "readouterr")()
|
|
76
|
-
assert "mock_order" not in captured.err, (
|
|
77
|
-
f"Expected no advisory when all fields present, got: {captured.err!r}"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def test_should_not_advise_for_incomplete_mocks_in_production_files(capsys: object) -> None:
|
|
82
|
-
source = (
|
|
83
|
-
"mock_order = {'id': 1}\n"
|
|
84
|
-
"\n"
|
|
85
|
-
"def run_order() -> None:\n"
|
|
86
|
-
" total = mock_order['total']\n"
|
|
87
|
-
)
|
|
88
|
-
code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_PRODUCTION_FILE_PATH)
|
|
89
|
-
captured = getattr(capsys, "readouterr")()
|
|
90
|
-
assert "mock_order" not in captured.err, (
|
|
91
|
-
f"Expected no advisory in production file, got: {captured.err!r}"
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def test_should_advise_for_attribute_access_on_mock_object(capsys: object) -> None:
|
|
96
|
-
source = (
|
|
97
|
-
"class MockUser:\n"
|
|
98
|
-
" pass\n"
|
|
99
|
-
"\n"
|
|
100
|
-
"mock_user = MockUser()\n"
|
|
101
|
-
"mock_user.name = 'Alice'\n"
|
|
102
|
-
"\n"
|
|
103
|
-
"def test_user_email() -> None:\n"
|
|
104
|
-
" email = mock_user.email\n"
|
|
105
|
-
" assert email\n"
|
|
106
|
-
)
|
|
107
|
-
code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
|
|
108
|
-
captured = getattr(capsys, "readouterr")()
|
|
109
|
-
assert "mock_user" in captured.err and "email" in captured.err, (
|
|
110
|
-
f"Expected advisory about missing 'email' attribute, got: {captured.err!r}"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def test_should_advise_when_mock_defined_inside_test_function_is_incomplete(
|
|
115
|
-
capsys: object,
|
|
116
|
-
) -> None:
|
|
117
|
-
source = (
|
|
118
|
-
"def test_thing() -> None:\n"
|
|
119
|
-
" mock_user = {'name': 'x'}\n"
|
|
120
|
-
" assert mock_user['email'] == 'y'\n"
|
|
121
|
-
)
|
|
122
|
-
code_rules_enforcer.check_incomplete_mocks(source, INCOMPLETE_MOCK_TEST_FILE_PATH)
|
|
123
|
-
captured = getattr(capsys, "readouterr")()
|
|
124
|
-
assert "mock_user" in captured.err and "email" in captured.err, (
|
|
125
|
-
f"Expected advisory for mock defined inside test function, got: {captured.err!r}"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def test_should_check_each_scope_mock_against_its_own_field_set(capsys: object) -> None:
|
|
130
|
-
"""Same mock_user name in two test functions with different field sets.
|
|
131
|
-
|
|
132
|
-
First function defines mock_user with only 'id'; accesses 'email' — should warn.
|
|
133
|
-
Second function defines mock_user with 'id' and 'email'; accesses 'email' — no warn.
|
|
134
|
-
The second definition must NOT overwrite the first scope's tracking.
|
|
135
|
-
"""
|
|
136
|
-
source = (
|
|
137
|
-
"def test_first_scope() -> None:\n"
|
|
138
|
-
" mock_user = {'id': 1}\n"
|
|
139
|
-
" email = mock_user['email']\n"
|
|
140
|
-
"\n"
|
|
141
|
-
"def test_second_scope() -> None:\n"
|
|
142
|
-
" mock_user = {'id': 2, 'email': 'b@b.com'}\n"
|
|
143
|
-
" email = mock_user['email']\n"
|
|
144
|
-
)
|
|
145
|
-
code_rules_enforcer.check_incomplete_mocks(source, SCOPE_KEYED_MOCK_TEST_FILE_PATH)
|
|
146
|
-
captured = getattr(capsys, "readouterr")()
|
|
147
|
-
advisory_lines = [
|
|
148
|
-
line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
|
|
149
|
-
]
|
|
150
|
-
assert len(advisory_lines) == 1, (
|
|
151
|
-
f"Expected exactly 1 advisory (first scope missing email), got: {captured.err!r}"
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def test_should_emit_exactly_one_advisory_for_repeated_accesses_to_same_missing_field(
|
|
156
|
-
capsys: object,
|
|
157
|
-
) -> None:
|
|
158
|
-
"""mock_user accessed 5 times for 'email' but email is missing — emit exactly one advisory."""
|
|
159
|
-
source = (
|
|
160
|
-
"def test_repeated_access() -> None:\n"
|
|
161
|
-
" mock_user = {'id': 1}\n"
|
|
162
|
-
" _ = mock_user['email']\n"
|
|
163
|
-
" _ = mock_user['email']\n"
|
|
164
|
-
" _ = mock_user['email']\n"
|
|
165
|
-
" _ = mock_user['email']\n"
|
|
166
|
-
" _ = mock_user['email']\n"
|
|
167
|
-
)
|
|
168
|
-
code_rules_enforcer.check_incomplete_mocks(source, SCOPE_KEYED_MOCK_TEST_FILE_PATH)
|
|
169
|
-
captured = getattr(capsys, "readouterr")()
|
|
170
|
-
advisory_lines = [
|
|
171
|
-
line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
|
|
172
|
-
]
|
|
173
|
-
assert len(advisory_lines) == 1, (
|
|
174
|
-
f"Expected exactly 1 advisory for 5 repeated accesses to missing 'email', got: {captured.err!r}"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def test_should_emit_exactly_one_advisory_for_module_level_mock_with_missing_field(
|
|
179
|
-
capsys: object,
|
|
180
|
-
) -> None:
|
|
181
|
-
"""Module-level mock_user with one missing field access should produce ONE advisory.
|
|
182
|
-
|
|
183
|
-
Finding 4: ast.walk() already yields the root Module node, so
|
|
184
|
-
[module_tree, *ast.walk(module_tree)] iterates the module twice and
|
|
185
|
-
previously produced two identical advisories for module-level mocks.
|
|
186
|
-
"""
|
|
187
|
-
source = (
|
|
188
|
-
"mock_user = {'name': 'Alice'}\n"
|
|
189
|
-
"\n"
|
|
190
|
-
"def test_email_present() -> None:\n"
|
|
191
|
-
" email = mock_user['email']\n"
|
|
192
|
-
" assert email\n"
|
|
193
|
-
)
|
|
194
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
195
|
-
captured = getattr(capsys, "readouterr")()
|
|
196
|
-
advisory_lines = [
|
|
197
|
-
line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
|
|
198
|
-
]
|
|
199
|
-
assert len(advisory_lines) == 1, (
|
|
200
|
-
f"Expected exactly 1 advisory for module-level mock missing 'email', got: {captured.err!r}"
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def test_should_not_leak_shadowed_nested_assignment_into_outer_mock_known_fields(
|
|
205
|
-
capsys: object,
|
|
206
|
-
) -> None:
|
|
207
|
-
"""Assignment collector must skip nested scopes that shadow the mock name.
|
|
208
|
-
|
|
209
|
-
The access collector uses _walk_scope_skipping_shadowed; the assignment
|
|
210
|
-
collector must do the same, otherwise attribute assignments inside a
|
|
211
|
-
nested function that redefines mock_user leak into the outer mock's
|
|
212
|
-
known-fields set and suppress advisories for genuinely missing fields.
|
|
213
|
-
"""
|
|
214
|
-
source = (
|
|
215
|
-
"mock_user = {'id': 1}\n"
|
|
216
|
-
"outer_value = mock_user['email']\n"
|
|
217
|
-
"\n"
|
|
218
|
-
"def test_inner() -> None:\n"
|
|
219
|
-
" mock_user = {'id': 2}\n"
|
|
220
|
-
" mock_user.email = 'shadowed@example.com'\n"
|
|
221
|
-
)
|
|
222
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
223
|
-
captured = getattr(capsys, "readouterr")()
|
|
224
|
-
advisory_lines = [
|
|
225
|
-
line for line in captured.err.splitlines() if "mock_user" in line and "email" in line
|
|
226
|
-
]
|
|
227
|
-
assert len(advisory_lines) == 1, (
|
|
228
|
-
"Expected outer mock's missing 'email' advisory to fire even when a shadowing "
|
|
229
|
-
f"nested function assigns mock_user.email, got: {captured.err!r}"
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def test_should_treat_annotated_assignment_as_shadowing_in_nested_scope(
|
|
234
|
-
capsys: object,
|
|
235
|
-
) -> None:
|
|
236
|
-
"""AnnAssign must shadow just like Assign.
|
|
237
|
-
|
|
238
|
-
When a nested scope re-binds the mock variable via an annotated
|
|
239
|
-
assignment (``mock_user: dict = {...}``), accesses inside that nested
|
|
240
|
-
scope belong to the inner mock, not the outer one. If the shadow
|
|
241
|
-
detector ignores AnnAssign, inner accesses leak out and cause
|
|
242
|
-
spurious advisories against the outer mock for fields it never sees.
|
|
243
|
-
"""
|
|
244
|
-
source = (
|
|
245
|
-
"mock_user = {'id': 1, 'name': 'outer'}\n"
|
|
246
|
-
"outer_value = mock_user['name']\n"
|
|
247
|
-
"\n"
|
|
248
|
-
"def test_inner() -> None:\n"
|
|
249
|
-
" mock_user: dict = {'id': 2, 'timezone': 'UTC'}\n"
|
|
250
|
-
" inner_value = mock_user['timezone']\n"
|
|
251
|
-
)
|
|
252
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
253
|
-
captured = getattr(capsys, "readouterr")()
|
|
254
|
-
leaked_advisories = [
|
|
255
|
-
line
|
|
256
|
-
for line in captured.err.splitlines()
|
|
257
|
-
if "mock_user" in line and "timezone" in line
|
|
258
|
-
]
|
|
259
|
-
assert leaked_advisories == [], (
|
|
260
|
-
"Expected no advisory on the outer mock for 'timezone' — that field is "
|
|
261
|
-
"accessed only inside a nested scope that re-binds mock_user via an "
|
|
262
|
-
f"annotated assignment, got: {captured.err!r}"
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def test_should_treat_assignment_inside_if_block_as_shadowing(capsys: object) -> None:
|
|
267
|
-
"""Binding inside an ``if`` block must shadow the outer mock name.
|
|
268
|
-
|
|
269
|
-
Python binds a name locally when it is assigned *anywhere* in the
|
|
270
|
-
function body, including inside a branch. A shadow detector that only
|
|
271
|
-
inspects the top-level statements misses this form.
|
|
272
|
-
"""
|
|
273
|
-
source = (
|
|
274
|
-
"mock_user = {'id': 1, 'name': 'outer'}\n"
|
|
275
|
-
"outer_value = mock_user['name']\n"
|
|
276
|
-
"\n"
|
|
277
|
-
"def test_inner() -> None:\n"
|
|
278
|
-
" if True:\n"
|
|
279
|
-
" mock_user = {'id': 2, 'timezone': 'UTC'}\n"
|
|
280
|
-
" inner_value = mock_user['timezone']\n"
|
|
281
|
-
)
|
|
282
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
283
|
-
captured = getattr(capsys, "readouterr")()
|
|
284
|
-
_assert_inner_field_did_not_leak(
|
|
285
|
-
captured.err, "timezone", "an assignment nested inside an if-block"
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def test_should_treat_for_loop_target_as_shadowing(capsys: object) -> None:
|
|
290
|
-
"""A ``for`` loop target binds the name locally and must shadow."""
|
|
291
|
-
source = (
|
|
292
|
-
"mock_user = {'id': 1, 'name': 'outer'}\n"
|
|
293
|
-
"outer_value = mock_user['name']\n"
|
|
294
|
-
"\n"
|
|
295
|
-
"def test_inner() -> None:\n"
|
|
296
|
-
" for mock_user in [{'id': 2, 'timezone': 'UTC'}]:\n"
|
|
297
|
-
" inner_value = mock_user['timezone']\n"
|
|
298
|
-
)
|
|
299
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
300
|
-
captured = getattr(capsys, "readouterr")()
|
|
301
|
-
_assert_inner_field_did_not_leak(
|
|
302
|
-
captured.err, "timezone", "a for-loop target"
|
|
303
|
-
)
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
"""Behavior tests for the code_rules_mock_completeness code-rules check module."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from types import SimpleNamespace
|
|
8
|
-
|
|
9
|
-
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
-
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
-
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
-
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
-
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
-
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
-
|
|
16
|
-
from code_rules_mock_completeness import ( # noqa: E402
|
|
17
|
-
check_incomplete_mocks,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
code_rules_enforcer = SimpleNamespace(
|
|
21
|
-
check_incomplete_mocks=check_incomplete_mocks,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
MODULE_LEVEL_MOCK_TEST_FILE_PATH = "packages/app/tests/test_module_level.py"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _assert_inner_field_did_not_leak(
|
|
29
|
-
captured_stderr: str,
|
|
30
|
-
inner_only_field_name: str,
|
|
31
|
-
binding_form_description: str,
|
|
32
|
-
) -> None:
|
|
33
|
-
leaked_advisories = [
|
|
34
|
-
line
|
|
35
|
-
for line in captured_stderr.splitlines()
|
|
36
|
-
if "mock_user" in line and inner_only_field_name in line
|
|
37
|
-
]
|
|
38
|
-
assert leaked_advisories == [], (
|
|
39
|
-
f"Expected no advisory on the outer mock for {inner_only_field_name!r} — "
|
|
40
|
-
f"that field is accessed only inside a nested scope that re-binds "
|
|
41
|
-
f"mock_user via {binding_form_description}, got: {captured_stderr!r}"
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def test_should_treat_try_except_handler_name_as_shadowing(capsys: object) -> None:
|
|
46
|
-
"""An ``except ... as mock_user`` handler binds the name locally."""
|
|
47
|
-
source = (
|
|
48
|
-
"mock_user = {'id': 1, 'name': 'outer'}\n"
|
|
49
|
-
"outer_value = mock_user['name']\n"
|
|
50
|
-
"\n"
|
|
51
|
-
"def test_inner() -> None:\n"
|
|
52
|
-
" try:\n"
|
|
53
|
-
" raise ValueError({'id': 2, 'timezone': 'UTC'})\n"
|
|
54
|
-
" except ValueError as mock_user:\n"
|
|
55
|
-
" inner_value = mock_user['timezone']\n"
|
|
56
|
-
)
|
|
57
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
58
|
-
captured = getattr(capsys, "readouterr")()
|
|
59
|
-
_assert_inner_field_did_not_leak(
|
|
60
|
-
captured.err, "timezone", "an except-handler binding"
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_should_treat_walrus_expression_as_shadowing(capsys: object) -> None:
|
|
65
|
-
"""A named-expression walrus binding inside a condition must shadow."""
|
|
66
|
-
source = (
|
|
67
|
-
"mock_user = {'id': 1, 'name': 'outer'}\n"
|
|
68
|
-
"outer_value = mock_user['name']\n"
|
|
69
|
-
"\n"
|
|
70
|
-
"def test_inner() -> None:\n"
|
|
71
|
-
" if (mock_user := {'id': 2, 'timezone': 'UTC'}):\n"
|
|
72
|
-
" inner_value = mock_user['timezone']\n"
|
|
73
|
-
)
|
|
74
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
75
|
-
captured = getattr(capsys, "readouterr")()
|
|
76
|
-
_assert_inner_field_did_not_leak(
|
|
77
|
-
captured.err, "timezone", "a walrus named-expression"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def test_should_treat_function_parameter_as_shadowing(capsys: object) -> None:
|
|
82
|
-
"""A parameter named like the mock variable must shadow the outer binding."""
|
|
83
|
-
source = (
|
|
84
|
-
"mock_user = {'id': 1, 'name': 'outer'}\n"
|
|
85
|
-
"outer_value = mock_user['name']\n"
|
|
86
|
-
"\n"
|
|
87
|
-
"def test_inner(mock_user: dict) -> None:\n"
|
|
88
|
-
" inner_value = mock_user['timezone']\n"
|
|
89
|
-
)
|
|
90
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
91
|
-
captured = getattr(capsys, "readouterr")()
|
|
92
|
-
_assert_inner_field_did_not_leak(
|
|
93
|
-
captured.err, "timezone", "a function parameter of the same name"
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_should_treat_import_asname_as_shadowing(capsys: object) -> None:
|
|
98
|
-
"""An ``import ... as mock_user`` must shadow the outer mock name."""
|
|
99
|
-
source = (
|
|
100
|
-
"mock_user = {'id': 1, 'name': 'outer'}\n"
|
|
101
|
-
"outer_value = mock_user['name']\n"
|
|
102
|
-
"\n"
|
|
103
|
-
"def test_inner() -> None:\n"
|
|
104
|
-
" import collections as mock_user\n"
|
|
105
|
-
" inner_value = mock_user['timezone']\n"
|
|
106
|
-
)
|
|
107
|
-
code_rules_enforcer.check_incomplete_mocks(source, MODULE_LEVEL_MOCK_TEST_FILE_PATH)
|
|
108
|
-
captured = getattr(capsys, "readouterr")()
|
|
109
|
-
_assert_inner_field_did_not_leak(
|
|
110
|
-
captured.err, "timezone", "an import-asname binding"
|
|
111
|
-
)
|