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.
Files changed (106) hide show
  1. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  2. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
  3. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  4. package/docs/CODE_RULES.md +3 -3
  5. package/docs/agent-spawn-protocol.md +1 -1
  6. package/docs/references/prose-style-enforcement.md +5 -9
  7. package/hooks/blocking/AGENTS.md +3 -13
  8. package/hooks/blocking/code_rules_docstrings.py +10 -2138
  9. package/hooks/blocking/code_rules_enforcer.py +0 -121
  10. package/hooks/blocking/code_rules_imports_logging.py +1 -236
  11. package/hooks/blocking/code_rules_shared.py +23 -0
  12. package/hooks/blocking/code_rules_test_layout.py +8 -8
  13. package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
  14. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
  15. package/hooks/blocking/precommit_code_rules_gate.py +8 -43
  16. package/hooks/blocking/state_description_blocker.py +1 -7
  17. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  18. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
  19. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
  20. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
  21. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
  22. package/hooks/blocking/test_code_rules_shared.py +12 -0
  23. package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
  24. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
  25. package/hooks/blocking/test_state_description_blocker.py +6 -4
  26. package/hooks/blocking/test_stop_dispatcher.py +5 -29
  27. package/hooks/hooks_constants/AGENTS.md +1 -6
  28. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
  29. package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
  30. package/hooks/hooks_constants/messages.py +0 -2
  31. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
  32. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
  33. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
  34. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
  35. package/hooks/hooks_constants/test_messages.py +5 -3
  36. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
  37. package/package.json +1 -1
  38. package/rules/AGENTS.md +3 -6
  39. package/rules/claims-as-quotes.md +0 -10
  40. package/rules/code-standards.md +6 -6
  41. package/rules/explore-thoroughly.md +0 -1
  42. package/rules/failure-blast-radius.md +0 -8
  43. package/rules/falsify-before-green.md +0 -8
  44. package/rules/file-global-constants.md +2 -2
  45. package/rules/filesystem-search.md +1 -1
  46. package/rules/git-workflow.md +1 -9
  47. package/rules/hedging-claims.md +2 -6
  48. package/rules/long-horizon-autonomy.md +1 -1
  49. package/rules/measurement-denominators.md +0 -9
  50. package/rules/research-mode.md +0 -6
  51. package/rules/verify-before-asking.md +0 -5
  52. package/rules/verify-runtime-state.md +0 -5
  53. package/scripts/codex_compat_materializer.py +0 -12
  54. package/scripts/sync_to_cursor/rules.py +0 -10
  55. package/scripts/tests/test_engine.py +0 -1
  56. package/scripts/tests/test_rules.py +0 -1
  57. package/scripts/tests/test_sync_to_cursor.py +0 -1
  58. package/system-prompts/software-engineer.xml +3 -3
  59. package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
  60. package/hooks/blocking/code_rules_dead_config_field.py +0 -568
  61. package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
  62. package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
  63. package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
  64. package/hooks/blocking/code_rules_mock_completeness.py +0 -295
  65. package/hooks/blocking/code_rules_scope_binding.py +0 -151
  66. package/hooks/blocking/code_rules_unused_imports.py +0 -197
  67. package/hooks/blocking/hedging_language_blocker.py +0 -221
  68. package/hooks/blocking/intent_only_ending_blocker.py +0 -148
  69. package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
  70. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
  71. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
  72. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
  73. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
  74. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
  75. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
  76. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
  77. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
  78. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
  79. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
  80. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
  81. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
  82. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
  83. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
  84. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
  86. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
  87. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
  88. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
  89. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
  90. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
  91. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
  92. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
  93. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
  94. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
  95. package/hooks/blocking/test_hedging_language_blocker.py +0 -261
  96. package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
  97. package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
  98. package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
  99. package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
  100. package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
  101. package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
  102. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
  103. package/rules/conservative-action.md +0 -17
  104. package/rules/context7.md +0 -8
  105. package/rules/few-words.md +0 -3
  106. 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
- )