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