claude-dev-env 1.24.0 → 1.25.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.
@@ -0,0 +1,116 @@
1
+ """Unit tests for code-rules-enforcer Any/type-ignore checks."""
2
+
3
+ import importlib.util
4
+ import pathlib
5
+ import sys
6
+
7
+ _HOOK_DIR = pathlib.Path(__file__).parent
8
+ if str(_HOOK_DIR) not in sys.path:
9
+ sys.path.insert(0, str(_HOOK_DIR))
10
+
11
+ hook_spec = importlib.util.spec_from_file_location(
12
+ "code_rules_enforcer",
13
+ _HOOK_DIR / "code-rules-enforcer.py",
14
+ )
15
+ assert hook_spec is not None
16
+ assert hook_spec.loader is not None
17
+ hook_module = importlib.util.module_from_spec(hook_spec)
18
+ hook_spec.loader.exec_module(hook_module)
19
+ check_type_escape_hatches = hook_module.check_type_escape_hatches
20
+
21
+ PRODUCTION_FILE_PATH = "/project/src/module.py"
22
+ TEST_FILE_PATH = "/project/src/test_module.py"
23
+
24
+
25
+ def test_should_flag_any_parameter_annotation() -> None:
26
+ source = "def foo(x: Any) -> None:\n pass\n"
27
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
28
+ assert any("Any" in issue for issue in issues)
29
+
30
+
31
+ def test_should_flag_any_return_annotation() -> None:
32
+ source = "def foo() -> Any:\n return None\n"
33
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
34
+ assert any("Any" in issue for issue in issues)
35
+
36
+
37
+ def test_should_flag_any_variable_annotation() -> None:
38
+ source = "x: Any = 1\n"
39
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
40
+ assert any("Any" in issue for issue in issues)
41
+
42
+
43
+ def test_should_flag_any_inside_optional() -> None:
44
+ source = "from typing import Optional\nx: Optional[Any] = None\n"
45
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
46
+ assert any("Any" in issue for issue in issues)
47
+
48
+
49
+ def test_should_allow_lowercase_any_as_builtin_call() -> None:
50
+ source = "items = [1, 2, 3]\nif any(x > 0 for x in items):\n pass\n"
51
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
52
+ assert not any("Any" in issue or "any" in issue for issue in issues)
53
+
54
+
55
+ def test_should_flag_bare_type_ignore() -> None:
56
+ source = "x = 1 # type: ignore\n"
57
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
58
+ assert any("type: ignore" in issue for issue in issues)
59
+
60
+
61
+ def test_should_flag_coded_type_ignore_without_justification() -> None:
62
+ source = "x = 1 # type: ignore[misc]\n"
63
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
64
+ assert any("type: ignore" in issue for issue in issues)
65
+
66
+
67
+ def test_should_allow_justified_type_ignore() -> None:
68
+ source = "x = 1 # type: ignore[misc] # stubs missing in foo library\n"
69
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
70
+ assert not any("type: ignore" in issue for issue in issues)
71
+
72
+
73
+ def test_should_skip_test_files() -> None:
74
+ source = "def foo(x: Any) -> Any:\n y: Any = 1 # type: ignore\n return y\n"
75
+ issues = check_type_escape_hatches(source, TEST_FILE_PATH)
76
+ assert issues == []
77
+
78
+
79
+ def test_should_flag_any_on_positional_only_parameter() -> None:
80
+ source = "def foo(x: Any, /) -> None:\n pass\n"
81
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
82
+ assert any("Any" in issue for issue in issues)
83
+
84
+
85
+ def test_should_flag_any_on_vararg() -> None:
86
+ source = "def foo(*args: Any) -> None:\n pass\n"
87
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
88
+ assert any("Any" in issue for issue in issues)
89
+
90
+
91
+ def test_should_flag_any_on_kwarg() -> None:
92
+ source = "def foo(**kwargs: Any) -> None:\n pass\n"
93
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
94
+ assert any("Any" in issue for issue in issues)
95
+
96
+
97
+ def test_should_not_flag_type_ignore_inside_string_literal() -> None:
98
+ source = 'message = "# type: ignore[misc] in docs"\n'
99
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
100
+ assert not any("type: ignore" in issue for issue in issues)
101
+
102
+
103
+ def test_should_report_any_and_type_ignore_together_without_cap_starvation() -> None:
104
+ any_lines = "\n".join(f"x{each_index}: Any = {each_index}" for each_index in range(5))
105
+ type_ignore_line = "y = 1 # type: ignore\n"
106
+ source = any_lines + "\n" + type_ignore_line
107
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
108
+ assert any("Any" in issue for issue in issues)
109
+ assert any("type: ignore" in issue for issue in issues)
110
+
111
+
112
+ def test_should_emit_unique_line_numbers_for_multiple_any_params_on_one_line() -> None:
113
+ source = "def foo(x: Any, y: Any, z: Any) -> None:\n pass\n"
114
+ issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
115
+ line_one_issues = [each_issue for each_issue in issues if each_issue.startswith("Line 1:") and "Any" in each_issue]
116
+ assert len(line_one_issues) <= 1
@@ -0,0 +1,231 @@
1
+ """Unit tests for banned-identifier check in code-rules-enforcer hook."""
2
+
3
+ import importlib.util
4
+ import pathlib
5
+ import sys
6
+
7
+ _HOOK_DIR = pathlib.Path(__file__).parent
8
+ if str(_HOOK_DIR) not in sys.path:
9
+ sys.path.insert(0, str(_HOOK_DIR))
10
+
11
+ hook_spec = importlib.util.spec_from_file_location(
12
+ "code_rules_enforcer",
13
+ _HOOK_DIR / "code-rules-enforcer.py",
14
+ )
15
+ assert hook_spec is not None
16
+ assert hook_spec.loader is not None
17
+ hook_module = importlib.util.module_from_spec(hook_spec)
18
+ hook_spec.loader.exec_module(hook_module)
19
+ check_banned_identifiers = hook_module.check_banned_identifiers
20
+
21
+ PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
22
+ TEST_FILE_PATH = "packages/app/services/test_loader.py"
23
+
24
+
25
+ def test_should_flag_result_assignment() -> None:
26
+ content = "def load():\n result = compute()\n return result\n"
27
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
28
+ assert any("result" in issue for issue in issues)
29
+
30
+
31
+ def test_should_flag_data_assignment() -> None:
32
+ content = "def fetch():\n data = read()\n return data\n"
33
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
34
+ assert any("data" in issue for issue in issues)
35
+
36
+
37
+ def test_should_flag_output_assignment() -> None:
38
+ content = "def render():\n output = build()\n return output\n"
39
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
40
+ assert any("output" in issue for issue in issues)
41
+
42
+
43
+ def test_should_flag_response_assignment() -> None:
44
+ content = "def call():\n response = send()\n return response\n"
45
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
46
+ assert any("response" in issue for issue in issues)
47
+
48
+
49
+ def test_should_flag_value_assignment() -> None:
50
+ content = "def read():\n value = lookup()\n return value\n"
51
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
52
+ assert any("value" in issue for issue in issues)
53
+
54
+
55
+ def test_should_flag_item_assignment() -> None:
56
+ content = "def pick():\n item = first()\n return item\n"
57
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
58
+ assert any("item" in issue for issue in issues)
59
+
60
+
61
+ def test_should_flag_temp_assignment() -> None:
62
+ content = "def swap():\n temp = holder()\n return temp\n"
63
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
64
+ assert any("temp" in issue for issue in issues)
65
+
66
+
67
+ def test_should_flag_annotated_assignment() -> None:
68
+ content = "def build() -> dict:\n data: dict = {}\n return data\n"
69
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
70
+ assert any("data" in issue for issue in issues)
71
+
72
+
73
+ def test_should_not_flag_descriptive_names() -> None:
74
+ content = (
75
+ "def summarize_orders():\n"
76
+ " all_users = load_users()\n"
77
+ " is_valid = True\n"
78
+ " price_by_product = {}\n"
79
+ " for each_order in all_users:\n"
80
+ " pass\n"
81
+ )
82
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
83
+ assert issues == []
84
+
85
+
86
+ def test_should_not_flag_name_containing_banned_substring() -> None:
87
+ content = (
88
+ "def aggregate():\n"
89
+ " result_set = fetch()\n"
90
+ " data_map = {}\n"
91
+ " value_counts = []\n"
92
+ " return result_set, data_map, value_counts\n"
93
+ )
94
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
95
+ assert issues == []
96
+
97
+
98
+ def test_should_skip_test_files() -> None:
99
+ content = "def test_thing():\n result = compute()\n assert result\n"
100
+ issues = check_banned_identifiers(content, TEST_FILE_PATH)
101
+ assert issues == []
102
+
103
+
104
+ def test_should_skip_hook_infrastructure() -> None:
105
+ hook_path = "/home/user/.claude/hooks/some-hook.py"
106
+ content = "def run():\n data = gather()\n return data\n"
107
+ issues = check_banned_identifiers(content, hook_path)
108
+ assert issues == []
109
+
110
+
111
+ def test_should_cap_at_three_issues() -> None:
112
+ content = (
113
+ "def many_bad():\n"
114
+ " result = 1\n"
115
+ " data = 2\n"
116
+ " output = 3\n"
117
+ " response = 4\n"
118
+ " value = 5\n"
119
+ " return result\n"
120
+ )
121
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
122
+ assert len(issues) == 3
123
+
124
+
125
+ def test_should_include_line_number_and_name() -> None:
126
+ content = "def run():\n result = 1\n return result\n"
127
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
128
+ assert len(issues) == 1
129
+ assert "Line 2" in issues[0]
130
+ assert "'result'" in issues[0]
131
+
132
+
133
+ def test_should_handle_syntax_error_gracefully() -> None:
134
+ content = "def broken(\n this is not python\n"
135
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
136
+ assert issues == []
137
+
138
+
139
+ def test_should_flag_tuple_unpacking_target() -> None:
140
+ content = "def run():\n result, err = compute()\n return result, err\n"
141
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
142
+ assert any("'result'" in issue for issue in issues)
143
+
144
+
145
+ def test_should_flag_list_unpacking_target() -> None:
146
+ content = "def run():\n [data, meta] = fetch()\n return data, meta\n"
147
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
148
+ assert any("'data'" in issue for issue in issues)
149
+
150
+
151
+ def test_should_flag_starred_unpacking_target() -> None:
152
+ content = "def run():\n head, *data = fetch()\n return head, data\n"
153
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
154
+ assert any("'data'" in issue for issue in issues)
155
+
156
+
157
+ def test_should_flag_for_loop_target() -> None:
158
+ content = "def run(orders):\n for result in orders:\n pass\n"
159
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
160
+ assert any("'result'" in issue for issue in issues)
161
+
162
+
163
+ def test_should_flag_async_for_loop_target() -> None:
164
+ content = (
165
+ "async def run(orders):\n"
166
+ " async for data in orders:\n"
167
+ " pass\n"
168
+ )
169
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
170
+ assert any("'data'" in issue for issue in issues)
171
+
172
+
173
+ def test_should_flag_list_comprehension_target() -> None:
174
+ content = "def run(rows):\n seen = [x for data in rows]\n return seen\n"
175
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
176
+ assert any("'data'" in issue for issue in issues)
177
+
178
+
179
+ def test_should_flag_dict_comprehension_target() -> None:
180
+ content = "def run(rows):\n mapping = {k: v for value in rows}\n return mapping\n"
181
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
182
+ assert any("'value'" in issue for issue in issues)
183
+
184
+
185
+ def test_should_flag_generator_expression_target() -> None:
186
+ content = "def run(rows):\n stream = (x for item in rows)\n return stream\n"
187
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
188
+ assert any("'item'" in issue for issue in issues)
189
+
190
+
191
+ def test_should_flag_with_as_target() -> None:
192
+ content = (
193
+ "def run():\n"
194
+ " with open('a') as data:\n"
195
+ " return data.read()\n"
196
+ )
197
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
198
+ assert any("'data'" in issue for issue in issues)
199
+
200
+
201
+ def test_should_flag_walrus_target() -> None:
202
+ content = "def run(source):\n if (data := source()):\n return data\n"
203
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
204
+ assert any("'data'" in issue for issue in issues)
205
+
206
+
207
+ def test_should_return_issues_in_source_line_order() -> None:
208
+ content = (
209
+ "def outer():\n"
210
+ " def inner():\n"
211
+ " result = 1\n"
212
+ " data = 2\n"
213
+ " return result\n"
214
+ " output = inner()\n"
215
+ " return output\n"
216
+ )
217
+ issues = check_banned_identifiers(content, PRODUCTION_FILE_PATH)
218
+ assert len(issues) == 3
219
+ assert "Line 3" in issues[0]
220
+ assert "Line 4" in issues[1]
221
+ assert "Line 6" in issues[2]
222
+
223
+
224
+ def test_should_emit_stderr_advisory_on_syntax_error(
225
+ capsys: "object",
226
+ ) -> None:
227
+ content = "def broken(\n this is not python\n"
228
+ check_banned_identifiers(content, PRODUCTION_FILE_PATH)
229
+ captured = capsys.readouterr() # type: ignore[attr-defined]
230
+ assert "banned-identifier check skipped" in captured.err
231
+ assert PRODUCTION_FILE_PATH in captured.err
@@ -0,0 +1,51 @@
1
+ """Tests ensuring the conftest pattern matches only the canonical filename."""
2
+
3
+ import importlib.util
4
+ from pathlib import Path
5
+
6
+
7
+ ENFORCER_MODULE_NAME = "code_rules_enforcer_under_test"
8
+ ENFORCER_SOURCE_PATH = Path(__file__).parent / "code-rules-enforcer.py"
9
+
10
+
11
+ def load_enforcer_module() -> object:
12
+ module_spec = importlib.util.spec_from_file_location(
13
+ ENFORCER_MODULE_NAME, ENFORCER_SOURCE_PATH
14
+ )
15
+ if module_spec is None or module_spec.loader is None:
16
+ raise RuntimeError(f"cannot load enforcer from {ENFORCER_SOURCE_PATH}")
17
+ enforcer_module = importlib.util.module_from_spec(module_spec)
18
+ module_spec.loader.exec_module(enforcer_module)
19
+ return enforcer_module
20
+
21
+
22
+ enforcer = load_enforcer_module()
23
+ is_test_file = enforcer.is_test_file
24
+
25
+
26
+ def test_is_test_file_should_match_canonical_conftest_py() -> None:
27
+ assert is_test_file("C:/proj/tests/conftest.py") is True
28
+
29
+
30
+ def test_is_test_file_should_match_nested_conftest_py() -> None:
31
+ assert is_test_file("C:/proj/subdir/conftest.py") is True
32
+
33
+
34
+ def test_is_test_file_should_not_match_conftest_substring_in_filename() -> None:
35
+ assert is_test_file("C:/proj/my_conftestfile.py") is False
36
+
37
+
38
+ def test_is_test_file_should_not_match_conftest_in_directory_name() -> None:
39
+ assert is_test_file("C:/proj/conftestdata/foo.py") is False
40
+
41
+
42
+ def test_is_test_file_should_not_match_prefixed_conftest_py() -> None:
43
+ assert is_test_file("C:/proj/myconftest.py") is False
44
+
45
+
46
+ def test_is_test_file_should_not_match_underscore_prefixed_conftest_py() -> None:
47
+ assert is_test_file("C:/proj/foo_conftest.py") is False
48
+
49
+
50
+ def test_is_test_file_should_match_conftest_py_with_backslash_separators() -> None:
51
+ assert is_test_file("C:\\proj\\subdir\\conftest.py") is True
@@ -0,0 +1,55 @@
1
+ """Regression tests for .test.{ts,tsx,js} recognition in code-rules-enforcer."""
2
+
3
+ import importlib.util
4
+ import pathlib
5
+
6
+
7
+ def _load_enforcer_module():
8
+ enforcer_path = pathlib.Path(__file__).parent / "code-rules-enforcer.py"
9
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", enforcer_path)
10
+ module = importlib.util.module_from_spec(spec)
11
+ spec.loader.exec_module(module)
12
+ return module
13
+
14
+
15
+ enforcer = _load_enforcer_module()
16
+
17
+
18
+ def test_is_test_file_should_recognize_dot_test_tsx_files():
19
+ assert enforcer.is_test_file("C:/foo/Button.test.tsx") is True
20
+
21
+
22
+ def test_is_test_file_should_recognize_dot_test_ts_files():
23
+ assert enforcer.is_test_file("C:/foo/Button.test.ts") is True
24
+
25
+
26
+ def test_is_test_file_should_recognize_dot_test_js_files():
27
+ assert enforcer.is_test_file("C:/foo/Button.test.js") is True
28
+
29
+
30
+ def test_is_test_file_should_still_recognize_python_test_files():
31
+ assert enforcer.is_test_file("C:/foo/test_foo.py") is True
32
+ assert enforcer.is_test_file("C:/foo/foo_test.py") is True
33
+ assert enforcer.is_test_file("C:/foo/conftest.py") is True
34
+ assert enforcer.is_test_file("C:/foo/foo.spec.ts") is True
35
+
36
+
37
+ def test_is_hook_infrastructure_should_recognize_packages_claude_dev_env_hooks_forward_slash():
38
+ assert enforcer.is_hook_infrastructure("/repo/packages/claude-dev-env/hooks/blocking/foo.py") is True
39
+
40
+
41
+ def test_is_hook_infrastructure_should_recognize_packages_claude_dev_env_hooks_backslash():
42
+ assert enforcer.is_hook_infrastructure("C:\\repo\\packages\\claude-dev-env\\hooks\\blocking\\foo.py") is True
43
+
44
+
45
+ def test_is_hook_infrastructure_should_recognize_packages_claude_dev_env_hooks_validators():
46
+ assert enforcer.is_hook_infrastructure("/repo/packages/claude-dev-env/hooks/validators/bar.py") is True
47
+
48
+
49
+ def test_is_hook_infrastructure_should_still_recognize_dot_claude_hooks():
50
+ assert enforcer.is_hook_infrastructure("C:/Users/jon/.claude/hooks/blocking/foo.py") is True
51
+ assert enforcer.is_hook_infrastructure("/home/user/.claude/hooks/blocking/foo.py") is True
52
+
53
+
54
+ def test_is_hook_infrastructure_should_not_match_unrelated_package_path():
55
+ assert enforcer.is_hook_infrastructure("/repo/packages/other-package/src/foo.py") is False
@@ -0,0 +1,144 @@
1
+ """Unit tests for code-rules-enforcer f-string structural literal scanner."""
2
+
3
+ import importlib.util
4
+ import pathlib
5
+ import sys
6
+
7
+ _HOOK_DIR = pathlib.Path(__file__).parent
8
+ if str(_HOOK_DIR) not in sys.path:
9
+ sys.path.insert(0, str(_HOOK_DIR))
10
+
11
+ hook_spec = importlib.util.spec_from_file_location(
12
+ "code_rules_enforcer",
13
+ _HOOK_DIR / "code-rules-enforcer.py",
14
+ )
15
+ assert hook_spec is not None
16
+ assert hook_spec.loader is not None
17
+ hook_module = importlib.util.module_from_spec(hook_spec)
18
+ hook_spec.loader.exec_module(hook_module)
19
+ check_fstring_structural_literals = hook_module.check_fstring_structural_literals
20
+ validate_content = hook_module.validate_content
21
+
22
+ PRODUCTION_FILE_PATH = "packages/claude-dev-env/example.py"
23
+ TEST_FILE_PATH = "packages/claude-dev-env/test_example.py"
24
+
25
+
26
+ def test_should_flag_fstring_with_url_path() -> None:
27
+ content = 'def build_url(user_id):\n endpoint = f"/api/v1/users/{user_id}"\n return endpoint\n'
28
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
29
+ assert issues, "expected URL path f-string to be flagged"
30
+
31
+
32
+ def test_should_flag_fstring_with_windows_path() -> None:
33
+ content = 'def build_path(name):\n location = f"C:\\\\Users\\\\{name}\\\\Documents"\n return location\n'
34
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
35
+ assert issues, "expected Windows path f-string to be flagged"
36
+
37
+
38
+ def test_should_flag_fstring_with_regex_pattern() -> None:
39
+ content = 'def build_pattern(group):\n regex = f"\\\\d+{group}\\\\w+"\n return regex\n'
40
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
41
+ assert issues, "expected regex metacharacter f-string to be flagged"
42
+
43
+
44
+ def test_should_not_flag_fstring_with_only_interpolation() -> None:
45
+ content = 'def render(value):\n rendered = f"{value}"\n return rendered\n'
46
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
47
+ assert issues == [], f"pure interpolation should not be flagged, got: {issues}"
48
+
49
+
50
+ def test_should_not_flag_fstring_with_trivial_separator() -> None:
51
+ content = 'def render(x):\n rendered = f"{x} "\n return rendered\n'
52
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
53
+ assert issues == [], f"single-space separator should not be flagged, got: {issues}"
54
+
55
+
56
+ def test_should_not_flag_true_false_literals() -> None:
57
+ content = "def toggle():\n x = True\n y = False\n return x, y\n"
58
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
59
+ assert issues == [], f"True/False literals should not be flagged, got: {issues}"
60
+
61
+
62
+ def test_should_not_flag_empty_string_literal() -> None:
63
+ content = 'def blank():\n x = ""\n return x\n'
64
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
65
+ assert issues == [], f"empty string literal should not be flagged, got: {issues}"
66
+
67
+
68
+ def test_should_skip_test_files() -> None:
69
+ content = 'def test_thing():\n url = f"/api/v1/users/{user_id}"\n'
70
+ issues_via_validate = validate_content(content, TEST_FILE_PATH, "")
71
+ fstring_issues = [
72
+ each_issue
73
+ for each_issue in issues_via_validate
74
+ if "structural" in each_issue.lower() or "f-string" in each_issue.lower()
75
+ ]
76
+ assert fstring_issues == [], (
77
+ f"test files should be exempt from f-string scanner, got: {fstring_issues}"
78
+ )
79
+
80
+
81
+ def test_should_not_flag_natural_english_with_single_slash() -> None:
82
+ content = 'def log_mode(mode):\n message = f"Test name contains online/offline - mode is {mode}"\n return message\n'
83
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
84
+ assert issues == [], (
85
+ f"natural English with single slash should not be flagged, got: {issues}"
86
+ )
87
+
88
+
89
+ def test_should_not_flag_common_english_slash_phrases() -> None:
90
+ for each_phrase in ("and/or", "CI/CD", "PR/MR", "input/output", "read/write"):
91
+ content = f'def note(x):\n message = f"{each_phrase} value is {{x}}"\n return message\n'
92
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
93
+ assert issues == [], (
94
+ f"phrase {each_phrase!r} should not be flagged, got: {issues}"
95
+ )
96
+
97
+
98
+ def test_should_flag_fstring_with_apostrophe() -> None:
99
+ content = 'def greet(name):\n message = f"it\'s /api/v1/{name}/home"\n return message\n'
100
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
101
+ assert issues, "f-string containing an apostrophe should still be detected"
102
+
103
+
104
+ def test_should_flag_triple_quoted_fstring_with_path() -> None:
105
+ content = 'def build(x):\n message = f"""/api/v1/{x}/path/extra"""\n return message\n'
106
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
107
+ assert issues, "triple-quoted f-string with path should be flagged"
108
+
109
+
110
+ def test_should_flag_raw_fstring_rf_prefix() -> None:
111
+ content = 'def build(x):\n message = rf"/api/v1/{x}/extra"\n return message\n'
112
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
113
+ assert issues, "rf-prefixed f-string with path should be flagged"
114
+
115
+
116
+ def test_should_flag_raw_fstring_fr_prefix() -> None:
117
+ content = 'def build(x):\n message = fr"/api/v1/{x}/extra"\n return message\n'
118
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
119
+ assert issues, "fr-prefixed f-string with path should be flagged"
120
+
121
+
122
+ def test_should_not_leak_escaped_braces_into_flag_message() -> None:
123
+ content = 'def build(x):\n message = f"{{/api/{x}/extra/path}}"\n return message\n'
124
+ issues = check_fstring_structural_literals(content, PRODUCTION_FILE_PATH)
125
+ for each_issue in issues:
126
+ assert "{{" not in each_issue, (
127
+ f"flag message should not contain escaped brace artifacts, got: {each_issue}"
128
+ )
129
+ assert "}}" not in each_issue, (
130
+ f"flag message should not contain escaped brace artifacts, got: {each_issue}"
131
+ )
132
+
133
+
134
+ def test_should_not_flag_enforcer_hook_itself() -> None:
135
+ hook_path = _HOOK_DIR / "code-rules-enforcer.py"
136
+ with open(hook_path, encoding="utf-8") as each_file:
137
+ enforcer_source = each_file.read()
138
+ issues = check_fstring_structural_literals(
139
+ enforcer_source,
140
+ "packages/claude-dev-env/hooks/blocking/code-rules-enforcer.py",
141
+ )
142
+ assert issues == [], (
143
+ f"the enforcer hook should not flag itself, got: {issues}"
144
+ )