claude-dev-env 1.44.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CLAUDE.md +9 -0
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +426 -85
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +20 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  5. package/_shared/pr-loop/scripts/reviews_disabled.py +82 -9
  6. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +630 -21
  7. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +15 -0
  8. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +57 -0
  9. package/agents/clean-coder.md +7 -1
  10. package/agents/code-quality-agent.md +8 -5
  11. package/hooks/blocking/code_rules_enforcer.py +1562 -37
  12. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +19 -0
  13. package/hooks/blocking/open_questions_in_plans_blocker.py +249 -0
  14. package/hooks/blocking/test_code_rules_enforcer.py +1389 -0
  15. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +292 -0
  16. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +46 -8
  17. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +189 -0
  18. package/hooks/blocking/test_code_rules_enforcer_function_length.py +210 -0
  19. package/hooks/blocking/test_code_rules_enforcer_tests_isolate_home_temp.py +1512 -0
  20. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +9 -5
  21. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +30 -0
  22. package/hooks/blocking/test_open_questions_in_plans_blocker.py +790 -0
  23. package/hooks/hooks.json +10 -0
  24. package/hooks/hooks_constants/banned_identifiers_constants.py +19 -0
  25. package/hooks/hooks_constants/code_rules_enforcer_constants.py +129 -2
  26. package/hooks/hooks_constants/open_questions_in_plans_blocker_constants.py +35 -0
  27. package/hooks/hooks_constants/test_open_questions_in_plans_blocker_constants.py +125 -0
  28. package/package.json +1 -1
  29. package/skills/_shared/pr-loop/scripts/_path_resolver.py +34 -13
  30. package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -2
  31. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -4
  32. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +57 -0
  33. package/skills/_shared/pr-loop/scripts/test_init_loop_state.py +48 -0
  34. package/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +59 -0
  35. package/skills/bugteam/PROMPTS.md +48 -12
  36. package/skills/bugteam/reference/team-setup.md +4 -2
  37. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +487 -76
  38. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +22 -1
  39. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +602 -12
  40. package/skills/pr-converge/SKILL.md +5 -0
  41. package/skills/pr-converge/reference/per-tick.md +14 -5
  42. package/skills/pr-converge/reference/state-schema.md +7 -3
  43. package/skills/pr-converge/scripts/check_convergence.py +27 -1
  44. package/skills/pr-converge/scripts/test_check_convergence.py +28 -0
@@ -0,0 +1,1512 @@
1
+ """Tests for ``check_tests_use_isolated_filesystem_paths``.
2
+
3
+ Pattern class: tests that call ``Path.home()``, ``os.path.expanduser('~')``,
4
+ ``os.getenv('HOME'|'USERPROFILE'|'TMPDIR'|…)``, ``os.environ['HOME'|…]``, or
5
+ ``tempfile.gettempdir()`` without taking a ``monkeypatch`` fixture leak across
6
+ the suite. Only ``monkeypatch`` suppresses the finding, because
7
+ ``monkeypatch.setenv(...)`` actually intercepts the env reads the probes
8
+ depend on. ``tmp_path``, ``tmp_path_factory``, ``tmpdir``, and
9
+ ``tmpdir_factory`` allocate a sandbox path but do not intercept env reads, so
10
+ their presence alone does not suppress the finding (see
11
+ ``test_should_flag_path_home_when_only_tmp_path_fixture_present``). Cited
12
+ SYNTHESIS evidence: ccc#476 F16, F19, F28; pa#136 F11.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import importlib.util
18
+ import pathlib
19
+ import sys
20
+
21
+ _HOOK_DIR = pathlib.Path(__file__).parent
22
+ if str(_HOOK_DIR) not in sys.path:
23
+ sys.path.insert(0, str(_HOOK_DIR))
24
+
25
+ hook_spec = importlib.util.spec_from_file_location(
26
+ "code_rules_enforcer",
27
+ _HOOK_DIR / "code_rules_enforcer.py",
28
+ )
29
+ assert hook_spec is not None
30
+ assert hook_spec.loader is not None
31
+ hook_module = importlib.util.module_from_spec(hook_spec)
32
+ hook_spec.loader.exec_module(hook_module)
33
+ check_tests_use_isolated_filesystem_paths = hook_module.check_tests_use_isolated_filesystem_paths
34
+
35
+ TEST_FILE_PATH = "/project/src/test_module.py"
36
+ PRODUCTION_FILE_PATH = "/project/src/module.py"
37
+
38
+
39
+ def test_should_flag_path_home_in_test_without_fixture() -> None:
40
+ source = (
41
+ "from pathlib import Path\n"
42
+ "def test_writes_dotfile() -> None:\n"
43
+ " home_dir = Path.home()\n"
44
+ " (home_dir / '.myapp').write_text('x')\n"
45
+ )
46
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
47
+ assert any("Path.home" in each_issue for each_issue in issues)
48
+
49
+
50
+ def test_changed_lines_scope_skips_untouched_probe() -> None:
51
+ """loop5-3: with changed_lines naming only an unrelated test, an untouched
52
+ HOME probe in another test must not be reported."""
53
+ source = (
54
+ "from pathlib import Path\n"
55
+ "def test_reads_home() -> None:\n"
56
+ " home_dir = Path.home()\n"
57
+ " assert home_dir\n"
58
+ "def test_addition() -> None:\n"
59
+ " assert 2 + 2 == 4\n"
60
+ )
61
+ addition_assert_line = 6
62
+ issues = check_tests_use_isolated_filesystem_paths(
63
+ source, TEST_FILE_PATH, all_changed_lines={addition_assert_line}
64
+ )
65
+ assert issues == [], f"untouched probe must not be in scope, got: {issues!r}"
66
+
67
+
68
+ def test_changed_lines_scope_keeps_touched_probe() -> None:
69
+ """loop5-3: when a changed line is the probe's source line, the violation
70
+ must remain reported."""
71
+ source = (
72
+ "from pathlib import Path\n"
73
+ "def test_reads_home() -> None:\n"
74
+ " home_dir = Path.home()\n"
75
+ " assert home_dir\n"
76
+ )
77
+ probe_line = 3
78
+ issues = check_tests_use_isolated_filesystem_paths(
79
+ source, TEST_FILE_PATH, all_changed_lines={probe_line}
80
+ )
81
+ assert any("Path.home" in each_issue for each_issue in issues)
82
+
83
+
84
+ def test_reports_only_in_scope_probe_among_untouched_ones() -> None:
85
+ """loop5-2: an in-scope probe appearing after several untouched out-of-scope
86
+ probes is still reported, while the untouched ones stay out of scope."""
87
+ leading_probe_count = 5
88
+ leading_tests = "".join(
89
+ f"def test_leading_{each_index}() -> None:\n"
90
+ f" leading_home = Path.home()\n"
91
+ f" assert leading_home\n"
92
+ for each_index in range(leading_probe_count)
93
+ )
94
+ header = "from pathlib import Path\n"
95
+ target_test = (
96
+ "def test_target() -> None:\n"
97
+ " target_home = Path.home()\n"
98
+ " assert target_home\n"
99
+ )
100
+ source = header + leading_tests + target_test
101
+ target_probe_line = len(source.splitlines()) - 1
102
+ issues = check_tests_use_isolated_filesystem_paths(
103
+ source, TEST_FILE_PATH, all_changed_lines={target_probe_line}
104
+ )
105
+ assert any("test_target" in each_issue for each_issue in issues)
106
+ assert not any("test_leading_" in each_issue for each_issue in issues)
107
+
108
+
109
+ def test_should_flag_path_home_when_only_tmp_path_fixture_present() -> None:
110
+ source = (
111
+ "from pathlib import Path\n"
112
+ "def test_writes_dotfile(tmp_path) -> None:\n"
113
+ " home_dir = Path.home()\n"
114
+ " (tmp_path / '.myapp').write_text('x')\n"
115
+ )
116
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
117
+ assert any("Path.home" in each_issue for each_issue in issues)
118
+
119
+
120
+ def test_should_flag_path_home_when_only_positional_only_tmp_path_present() -> None:
121
+ source = (
122
+ "from pathlib import Path\n"
123
+ "def test_writes_dotfile(tmp_path, /) -> None:\n"
124
+ " home_dir = Path.home()\n"
125
+ " (tmp_path / '.myapp').write_text('x')\n"
126
+ )
127
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
128
+ assert any("Path.home" in each_issue for each_issue in issues)
129
+
130
+
131
+ def test_should_allow_path_home_in_test_with_positional_only_monkeypatch_fixture() -> None:
132
+ source = (
133
+ "from pathlib import Path\n"
134
+ "def test_writes_dotfile(monkeypatch, /) -> None:\n"
135
+ " monkeypatch.setenv('HOME', '/tmp/fake')\n"
136
+ " home_dir = Path.home()\n"
137
+ )
138
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
139
+ assert issues == []
140
+
141
+
142
+ def test_should_ignore_path_home_inside_nested_helper_function() -> None:
143
+ source = (
144
+ "from pathlib import Path\n"
145
+ "def test_writes_dotfile() -> None:\n"
146
+ " def _nested_helper() -> Path:\n"
147
+ " return Path.home()\n"
148
+ " assert callable(_nested_helper)\n"
149
+ )
150
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
151
+ assert issues == []
152
+
153
+
154
+ def test_should_ignore_path_home_inside_nested_lambda() -> None:
155
+ source = (
156
+ "from pathlib import Path\n"
157
+ "def test_makes_lambda() -> None:\n"
158
+ " lookup_home = lambda: Path.home()\n"
159
+ " assert callable(lookup_home)\n"
160
+ )
161
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
162
+ assert issues == []
163
+
164
+
165
+ def test_should_flag_path_home_inside_nested_class_body() -> None:
166
+ # A class-level statement directly in a nested class body runs at
167
+ # class-creation time during the test, so a Path.home() initializer there
168
+ # executes on the test's runtime path and must be flagged.
169
+ source = (
170
+ "from pathlib import Path\n"
171
+ "def test_defines_inner_class() -> None:\n"
172
+ " class Inner:\n"
173
+ " root = Path.home()\n"
174
+ " assert Inner is not None\n"
175
+ )
176
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
177
+ assert any("Path.home" in each_issue for each_issue in issues)
178
+
179
+
180
+ def test_should_ignore_path_home_inside_uncalled_nested_class_method() -> None:
181
+ # An ordinary method of a nested class does not run merely because the
182
+ # class is defined during the test; Python only executes a method when it
183
+ # is called. A Path.home() in the body of an uncalled method is therefore
184
+ # not on the test's runtime path and must not be flagged.
185
+ source = (
186
+ "from pathlib import Path\n"
187
+ "def test_defines_inner_class() -> None:\n"
188
+ " class Inner:\n"
189
+ " def resolve_root(self) -> Path:\n"
190
+ " return Path.home()\n"
191
+ " assert Inner is not None\n"
192
+ )
193
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
194
+ assert issues == [], (
195
+ "an uncalled nested-class method body does not execute during the test, "
196
+ f"so its Path.home() must not be flagged; got: {issues!r}"
197
+ )
198
+
199
+
200
+ def test_should_ignore_path_home_inside_nested_class_method_lambda() -> None:
201
+ # A lambda defined inside a nested class method is two callable scopes
202
+ # removed from the test path; neither the method nor the lambda runs from
203
+ # the class definition alone.
204
+ source = (
205
+ "from pathlib import Path\n"
206
+ "def test_defines_inner_class() -> None:\n"
207
+ " class Inner:\n"
208
+ " def build(self) -> object:\n"
209
+ " return lambda: Path.home()\n"
210
+ " assert Inner is not None\n"
211
+ )
212
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
213
+ assert issues == [], (
214
+ "a lambda inside an uncalled nested-class method must not be flagged; "
215
+ f"got: {issues!r}"
216
+ )
217
+
218
+
219
+ def test_should_ignore_nested_test_named_function_pytest_does_not_collect() -> None:
220
+ source = (
221
+ "from pathlib import Path\n"
222
+ "def test_outer_caller(monkeypatch) -> None:\n"
223
+ " monkeypatch.setenv('HOME', '/tmp/fake')\n"
224
+ " def test_home_helper() -> None:\n"
225
+ " Path.home()\n"
226
+ " assert callable(test_home_helper)\n"
227
+ )
228
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
229
+ assert issues == []
230
+
231
+
232
+ def test_should_still_flag_path_home_at_top_level_when_nested_helper_also_probes() -> None:
233
+ source = (
234
+ "from pathlib import Path\n"
235
+ "def test_top_level_probe_survives_nested_scope() -> None:\n"
236
+ " target = Path.home() / '.myapp'\n"
237
+ " def _nested_helper() -> Path:\n"
238
+ " return Path.home()\n"
239
+ " target.write_text('x')\n"
240
+ )
241
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
242
+ assert any("Path.home" in each_issue for each_issue in issues)
243
+ assert len(issues) == 1
244
+
245
+
246
+ def test_should_allow_path_home_in_test_with_monkeypatch_fixture() -> None:
247
+ source = (
248
+ "from pathlib import Path\n"
249
+ "def test_writes_dotfile(monkeypatch, tmp_path) -> None:\n"
250
+ " monkeypatch.setenv('HOME', str(tmp_path))\n"
251
+ " home_dir = Path.home()\n"
252
+ )
253
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
254
+ assert issues == []
255
+
256
+
257
+ def test_should_flag_expanduser_call_without_isolation() -> None:
258
+ source = (
259
+ "import os\n"
260
+ "def test_reads_dotfile() -> None:\n"
261
+ " target = os.path.expanduser('~/.config/x')\n"
262
+ " open(target).read()\n"
263
+ )
264
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
265
+ assert any("expanduser" in each_issue for each_issue in issues)
266
+
267
+
268
+ def test_should_flag_tempfile_gettempdir_without_isolation() -> None:
269
+ source = (
270
+ "import tempfile\n"
271
+ "def test_writes_to_shared_temp() -> None:\n"
272
+ " base = tempfile.gettempdir()\n"
273
+ " (base + '/x.txt')\n"
274
+ )
275
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
276
+ assert any("gettempdir" in each_issue for each_issue in issues)
277
+
278
+
279
+ def test_should_flag_os_environ_subscript_for_home() -> None:
280
+ source = (
281
+ "import os\n"
282
+ "def test_resolves_home() -> None:\n"
283
+ " home = os.environ['HOME']\n"
284
+ " print(home)\n"
285
+ )
286
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
287
+ assert any("HOME" in each_issue for each_issue in issues)
288
+
289
+
290
+ def test_should_flag_os_environ_subscript_for_userprofile() -> None:
291
+ source = (
292
+ "import os\n"
293
+ "def test_resolves_userprofile() -> None:\n"
294
+ " user = os.environ['USERPROFILE']\n"
295
+ " print(user)\n"
296
+ )
297
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
298
+ assert any("USERPROFILE" in each_issue for each_issue in issues)
299
+
300
+
301
+ def test_should_flag_os_getenv_for_tmpdir() -> None:
302
+ source = (
303
+ "import os\n"
304
+ "def test_resolves_tmpdir() -> None:\n"
305
+ " tmp_root = os.getenv('TMPDIR')\n"
306
+ " print(tmp_root)\n"
307
+ )
308
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
309
+ assert any("TMPDIR" in each_issue for each_issue in issues)
310
+
311
+
312
+ def test_should_not_flag_os_getenv_for_unrelated_var() -> None:
313
+ source = (
314
+ "import os\n"
315
+ "def test_unrelated_env() -> None:\n"
316
+ " value = os.getenv('MY_APP_TOKEN')\n"
317
+ " print(value)\n"
318
+ )
319
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
320
+ assert issues == []
321
+
322
+
323
+ def test_should_flag_expandvars_referencing_home_env_var() -> None:
324
+ source = (
325
+ "import os\n"
326
+ "def test_expands_home() -> None:\n"
327
+ " target = os.path.expandvars('$HOME/.config/x')\n"
328
+ " open(target).read()\n"
329
+ )
330
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
331
+ assert any("expandvars" in each_issue for each_issue in issues)
332
+
333
+
334
+ def test_should_flag_expandvars_referencing_temp_env_var() -> None:
335
+ source = (
336
+ "import os\n"
337
+ "def test_expands_temp() -> None:\n"
338
+ " target = os.path.expandvars('$TEMP/scratch')\n"
339
+ " open(target).read()\n"
340
+ )
341
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
342
+ assert any("expandvars" in each_issue for each_issue in issues)
343
+
344
+
345
+ def test_should_flag_expandvars_with_braced_home_reference() -> None:
346
+ source = (
347
+ "import os\n"
348
+ "def test_expands_braced_home() -> None:\n"
349
+ " target = os.path.expandvars('${USERPROFILE}/Documents')\n"
350
+ " open(target).read()\n"
351
+ )
352
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
353
+ assert any("expandvars" in each_issue for each_issue in issues)
354
+
355
+
356
+ def test_should_not_flag_expandvars_referencing_unrelated_var() -> None:
357
+ source = (
358
+ "import os\n"
359
+ "def test_expands_unrelated() -> None:\n"
360
+ " token = os.path.expandvars('$MY_APP_TOKEN')\n"
361
+ " print(token)\n"
362
+ )
363
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
364
+ assert issues == []
365
+
366
+
367
+ def test_should_flag_bare_imported_expanduser() -> None:
368
+ source = (
369
+ "from os.path import expanduser\n"
370
+ "def test_reads_dotfile() -> None:\n"
371
+ " target = expanduser('~/.config/x')\n"
372
+ " open(target).read()\n"
373
+ )
374
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
375
+ assert any("expanduser" in each_issue for each_issue in issues)
376
+
377
+
378
+ def test_should_flag_bare_imported_expanduser_under_alias() -> None:
379
+ source = (
380
+ "from os.path import expanduser as expand_home\n"
381
+ "def test_reads_dotfile() -> None:\n"
382
+ " target = expand_home('~/.config/x')\n"
383
+ " open(target).read()\n"
384
+ )
385
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
386
+ assert any("expanduser" in each_issue for each_issue in issues)
387
+
388
+
389
+ def test_should_flag_aliased_os_path_module_expanduser() -> None:
390
+ source = (
391
+ "import os.path as op\n"
392
+ "def test_reads_dotfile() -> None:\n"
393
+ " target = op.expanduser('~/.config/x')\n"
394
+ " open(target).read()\n"
395
+ )
396
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
397
+ assert any("expanduser" in each_issue for each_issue in issues)
398
+
399
+
400
+ def test_should_flag_bare_imported_getenv_for_home() -> None:
401
+ source = (
402
+ "from os import getenv\n"
403
+ "def test_resolves_home() -> None:\n"
404
+ " home = getenv('HOME')\n"
405
+ " print(home)\n"
406
+ )
407
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
408
+ assert any("HOME" in each_issue for each_issue in issues)
409
+
410
+
411
+ def test_should_not_flag_bare_imported_getenv_for_unrelated_var() -> None:
412
+ source = (
413
+ "from os import getenv\n"
414
+ "def test_resolves_token() -> None:\n"
415
+ " token = getenv('MY_APP_TOKEN')\n"
416
+ " print(token)\n"
417
+ )
418
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
419
+ assert issues == []
420
+
421
+
422
+ def test_should_flag_aliased_os_module_path_expanduser() -> None:
423
+ source = (
424
+ "import os as o\n"
425
+ "def test_reads_dotfile() -> None:\n"
426
+ " target = o.path.expanduser('~/.config/x')\n"
427
+ " open(target).read()\n"
428
+ )
429
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
430
+ assert any("expanduser" in each_issue for each_issue in issues)
431
+
432
+
433
+ def test_should_flag_aliased_os_module_getenv_for_home() -> None:
434
+ source = (
435
+ "import os as o\n"
436
+ "def test_resolves_home() -> None:\n"
437
+ " home = o.getenv('HOME')\n"
438
+ " print(home)\n"
439
+ )
440
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
441
+ assert any("HOME" in each_issue for each_issue in issues)
442
+
443
+
444
+ def test_should_not_flag_aliased_os_module_getenv_for_unrelated_var() -> None:
445
+ source = (
446
+ "import os as o\n"
447
+ "def test_resolves_token() -> None:\n"
448
+ " token = o.getenv('MY_APP_TOKEN')\n"
449
+ " print(token)\n"
450
+ )
451
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
452
+ assert issues == []
453
+
454
+
455
+ def test_should_flag_os_environ_via_local_binding() -> None:
456
+ source = (
457
+ "import os\n"
458
+ "def test_resolves_home() -> None:\n"
459
+ " e = os.environ\n"
460
+ " home = e['HOME']\n"
461
+ " print(home)\n"
462
+ )
463
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
464
+ assert any("HOME" in each_issue for each_issue in issues)
465
+
466
+
467
+ def test_should_not_flag_os_environ_local_binding_for_unrelated_var() -> None:
468
+ source = (
469
+ "import os\n"
470
+ "def test_resolves_token() -> None:\n"
471
+ " e = os.environ\n"
472
+ " token = e['MY_APP_TOKEN']\n"
473
+ " print(token)\n"
474
+ )
475
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
476
+ assert issues == []
477
+
478
+
479
+ def test_should_flag_os_environ_get_via_local_binding() -> None:
480
+ source = (
481
+ "import os\n"
482
+ "def test_resolves_home() -> None:\n"
483
+ " e = os.environ\n"
484
+ " home = e.get('HOME')\n"
485
+ " print(home)\n"
486
+ )
487
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
488
+ assert any("HOME" in each_issue for each_issue in issues)
489
+
490
+
491
+ def test_should_not_flag_os_environ_get_local_binding_for_unrelated_var() -> None:
492
+ source = (
493
+ "import os\n"
494
+ "def test_resolves_token() -> None:\n"
495
+ " e = os.environ\n"
496
+ " token = e.get('MY_APP_TOKEN')\n"
497
+ " print(token)\n"
498
+ )
499
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
500
+ assert issues == []
501
+
502
+
503
+ def test_should_flag_canonical_os_environ_get_for_home() -> None:
504
+ source = (
505
+ "import os\n"
506
+ "def test_resolves_home() -> None:\n"
507
+ " home = os.environ.get('HOME')\n"
508
+ " print(home)\n"
509
+ )
510
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
511
+ assert any("HOME" in each_issue for each_issue in issues)
512
+
513
+
514
+ def test_should_not_flag_path_binding_leaking_from_a_different_test() -> None:
515
+ # The Path-binding collector must scope its bindings to the test currently
516
+ # being analyzed. A `p = Path('~/x')` binding in test_a must NOT make an
517
+ # unrelated `p.expanduser()` in test_b a finding (test_b never bound `p`
518
+ # to a Path). Module-wide binding collection produces this false positive.
519
+ source = (
520
+ "from pathlib import Path\n"
521
+ "def test_a() -> None:\n"
522
+ " p = Path('~/x')\n"
523
+ " p.expanduser()\n"
524
+ "def test_b(p) -> None:\n"
525
+ " p.expanduser()\n"
526
+ )
527
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
528
+ assert any("test_a" in each_issue for each_issue in issues)
529
+ assert not any("test_b" in each_issue for each_issue in issues)
530
+
531
+
532
+ def test_should_flag_path_binding_used_within_the_same_test() -> None:
533
+ source = (
534
+ "from pathlib import Path\n"
535
+ "def test_within_one_test() -> None:\n"
536
+ " p = Path('~/x')\n"
537
+ " p.expanduser()\n"
538
+ )
539
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
540
+ assert any("expanduser" in each_issue for each_issue in issues)
541
+ assert any("test_within_one_test" in each_issue for each_issue in issues)
542
+
543
+
544
+ def test_should_not_flag_environ_binding_leaking_from_a_different_test() -> None:
545
+ # The environ-binding collector must scope its bindings to the test
546
+ # currently being analyzed. An `e = os.environ` binding in test_a must NOT
547
+ # make an unrelated `e['HOME']` in test_b a finding (test_b never bound
548
+ # `e` to os.environ). Module-wide binding collection produces this false
549
+ # positive.
550
+ source = (
551
+ "import os\n"
552
+ "def test_a() -> None:\n"
553
+ " e = os.environ\n"
554
+ " home = e['HOME']\n"
555
+ " print(home)\n"
556
+ "def test_b(e) -> None:\n"
557
+ " home = e['HOME']\n"
558
+ " print(home)\n"
559
+ )
560
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
561
+ assert any("test_a" in each_issue for each_issue in issues)
562
+ assert not any("test_b" in each_issue for each_issue in issues)
563
+
564
+
565
+ def test_should_flag_environ_binding_used_within_the_same_test() -> None:
566
+ source = (
567
+ "import os\n"
568
+ "def test_within_one_test() -> None:\n"
569
+ " e = os.environ\n"
570
+ " home = e['HOME']\n"
571
+ " print(home)\n"
572
+ )
573
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
574
+ assert any("HOME" in each_issue for each_issue in issues)
575
+ assert any("test_within_one_test" in each_issue for each_issue in issues)
576
+
577
+
578
+ def test_should_flag_module_level_from_os_import_environ_subscript() -> None:
579
+ source = (
580
+ "from os import environ\n"
581
+ "def test_resolves_home() -> None:\n"
582
+ " home = environ['HOME']\n"
583
+ " print(home)\n"
584
+ )
585
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
586
+ assert any("HOME" in each_issue for each_issue in issues)
587
+ assert any("test_resolves_home" in each_issue for each_issue in issues)
588
+
589
+
590
+ def test_should_not_flag_module_level_from_os_import_environ_for_unrelated_var() -> None:
591
+ source = (
592
+ "from os import environ\n"
593
+ "def test_resolves_token() -> None:\n"
594
+ " token = environ['MY_APP_TOKEN']\n"
595
+ " print(token)\n"
596
+ )
597
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
598
+ assert issues == []
599
+
600
+
601
+ def test_should_ignore_path_home_inside_nested_function_within_nested_class_method() -> None:
602
+ # A function nested inside an uncalled nested-class method is two callable
603
+ # scopes removed from the test path. Neither the method nor the inner
604
+ # function runs from the class definition alone, so the probe must not be
605
+ # flagged.
606
+ source = (
607
+ "from pathlib import Path\n"
608
+ "class TestFoo:\n"
609
+ " def test_unsafe(self) -> None:\n"
610
+ " class HomePath:\n"
611
+ " def build(self) -> Path:\n"
612
+ " def _inner() -> Path:\n"
613
+ " return Path.home()\n"
614
+ " return _inner()\n"
615
+ " h = HomePath()\n"
616
+ " assert h is not None\n"
617
+ )
618
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
619
+ assert issues == [], (
620
+ "a probe inside an uncalled nested-class method body must stay out of "
621
+ f"scope; got: {issues!r}"
622
+ )
623
+
624
+
625
+ def test_should_ignore_lambda_probe_inside_nested_class_method() -> None:
626
+ # A lambda body inside a nested-class method runs only when the method
627
+ # runs, which the class definition alone does not trigger. The method body
628
+ # is a callable-scope boundary, so the lambda probe must not be flagged.
629
+ source = (
630
+ "from pathlib import Path\n"
631
+ "class TestFoo:\n"
632
+ " def test_unsafe(self) -> None:\n"
633
+ " class HomePath:\n"
634
+ " def build(self):\n"
635
+ " return (lambda: Path.home())()\n"
636
+ " h = HomePath()\n"
637
+ " assert h is not None\n"
638
+ )
639
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
640
+ assert issues == [], (
641
+ "a lambda probe inside an uncalled nested-class method body must stay "
642
+ f"out of scope; got: {issues!r}"
643
+ )
644
+
645
+
646
+ def test_should_not_run_on_production_files() -> None:
647
+ source = (
648
+ "from pathlib import Path\ndef test_writes_dotfile() -> None:\n home_dir = Path.home()\n"
649
+ )
650
+ issues = check_tests_use_isolated_filesystem_paths(source, PRODUCTION_FILE_PATH)
651
+ assert issues == []
652
+
653
+
654
+ def test_should_ignore_module_level_helpers_in_test_files() -> None:
655
+ source = (
656
+ "from pathlib import Path\n"
657
+ "def helper_paths() -> Path:\n"
658
+ " return Path.home()\n"
659
+ "def test_uses_helper(tmp_path) -> None:\n"
660
+ " helper_paths()\n"
661
+ )
662
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
663
+ assert issues == []
664
+
665
+
666
+ def test_should_ignore_helper_named_with_bare_test_prefix() -> None:
667
+ source = (
668
+ "from pathlib import Path\n"
669
+ "def testing_factory() -> Path:\n"
670
+ " return Path.home()\n"
671
+ "def testify_connection() -> Path:\n"
672
+ " return Path.home()\n"
673
+ "def testament_root() -> Path:\n"
674
+ " return Path.home()\n"
675
+ )
676
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
677
+ assert issues == []
678
+
679
+
680
+ def test_should_flag_aliased_path_home_probe() -> None:
681
+ source = (
682
+ "from pathlib import Path as P\n"
683
+ "def test_writes_dotfile() -> None:\n"
684
+ " home_dir = P.home()\n"
685
+ " (home_dir / '.myapp').write_text('x')\n"
686
+ )
687
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
688
+ assert any("home" in each_issue.lower() for each_issue in issues)
689
+
690
+
691
+ def test_should_flag_aliased_module_import_home_probe() -> None:
692
+ source = (
693
+ "import pathlib as pathlib_alias\n"
694
+ "def test_writes_dotfile() -> None:\n"
695
+ " home_dir = pathlib_alias.Path.home()\n"
696
+ " (home_dir / '.myapp').write_text('x')\n"
697
+ )
698
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
699
+ assert any("home" in each_issue.lower() for each_issue in issues)
700
+
701
+
702
+ def test_should_allow_aliased_path_home_with_monkeypatch_fixture() -> None:
703
+ source = (
704
+ "from pathlib import Path as P\n"
705
+ "def test_writes_dotfile(monkeypatch) -> None:\n"
706
+ " monkeypatch.setenv('HOME', '/tmp/fake')\n"
707
+ " home_dir = P.home()\n"
708
+ )
709
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
710
+ assert issues == []
711
+
712
+
713
+ def test_should_handle_async_test_functions() -> None:
714
+ source = (
715
+ "from pathlib import Path\n"
716
+ "async def test_writes_dotfile() -> None:\n"
717
+ " home_dir = Path.home()\n"
718
+ )
719
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
720
+ assert any("Path.home" in each_issue for each_issue in issues)
721
+
722
+
723
+ def test_should_recognize_should_prefix_functions_as_tests() -> None:
724
+ source = (
725
+ "from pathlib import Path\n"
726
+ "def should_write_dotfile() -> None:\n"
727
+ " home_dir = Path.home()\n"
728
+ )
729
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
730
+ assert any("Path.home" in each_issue for each_issue in issues)
731
+
732
+
733
+ def test_should_skip_when_source_fails_to_parse() -> None:
734
+ source = "def test_broken(:\n"
735
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
736
+ assert issues == []
737
+
738
+
739
+ def test_edit_drops_every_out_of_scope_probe() -> None:
740
+ """An edit that touches none of the probe lines reports nothing — every
741
+ probe is out of scope (untouched code must not block a single-file edit), so
742
+ the cap has nothing in scope to preserve."""
743
+ repeated_probes = "\n".join(f" p{each_index} = Path.home()" for each_index in range(20))
744
+ source = f"from pathlib import Path\ndef test_many_probes() -> None:\n{repeated_probes}\n"
745
+ untouched_line_far_outside_any_probe = 100000
746
+ issues = check_tests_use_isolated_filesystem_paths(
747
+ source,
748
+ TEST_FILE_PATH,
749
+ all_changed_lines={untouched_line_far_outside_any_probe},
750
+ )
751
+ assert issues == []
752
+
753
+
754
+ def test_new_file_reports_every_probe_uncapped() -> None:
755
+ """On a new file (``all_changed_lines is None``) every line is in scope, so
756
+ the cap must not drop a probe — all are reported."""
757
+ probe_count = 20
758
+ repeated_probes = "\n".join(
759
+ f" p{each_index} = Path.home()" for each_index in range(probe_count)
760
+ )
761
+ source = f"from pathlib import Path\ndef test_many_probes() -> None:\n{repeated_probes}\n"
762
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
763
+ assert len(issues) == probe_count
764
+
765
+
766
+ def test_should_ignore_test_method_inside_non_test_prefixed_helper_class() -> None:
767
+ # Helper classes (non-Test* prefix) are not collected by pytest under the
768
+ # repo's `python_classes = Test*` setting, so methods on them must not
769
+ # produce HOME/TMP isolation findings.
770
+ source = (
771
+ "from pathlib import Path\n"
772
+ "class HelperFactory:\n"
773
+ " def test_makes_home_probe(self) -> Path:\n"
774
+ " return Path.home()\n"
775
+ )
776
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
777
+ assert issues == []
778
+
779
+
780
+ def test_should_flag_test_method_inside_test_prefixed_class() -> None:
781
+ # Test*-prefixed classes ARE collected by pytest under the repo's
782
+ # `python_classes = Test*` setting, so methods on them must still produce
783
+ # HOME/TMP isolation findings.
784
+ source = (
785
+ "from pathlib import Path\n"
786
+ "class TestHomeProbing:\n"
787
+ " def test_makes_home_probe(self) -> None:\n"
788
+ " home_dir = Path.home()\n"
789
+ " (home_dir / '.myapp').write_text('x')\n"
790
+ )
791
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
792
+ assert any("Path.home" in each_issue for each_issue in issues)
793
+
794
+
795
+ def test_should_ignore_path_home_inside_nested_class_method_of_outer_test() -> None:
796
+ # A method of a nested class is a callable-scope boundary. Python does not
797
+ # run a method just because its class is defined, and static analysis
798
+ # cannot reliably tell which methods a later instantiation calls. The
799
+ # walker treats every nested-class method body as a boundary, so a
800
+ # Path.home() in __init__ is not attributed to the outer test.
801
+ source = (
802
+ "from pathlib import Path\n"
803
+ "class TestFoo:\n"
804
+ " def test_unsafe(self) -> None:\n"
805
+ " class HomePath:\n"
806
+ " def __init__(self) -> None:\n"
807
+ " self.real_home = Path.home()\n"
808
+ " h = HomePath()\n"
809
+ " assert h is not None\n"
810
+ )
811
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
812
+ assert issues == [], (
813
+ "a probe inside a nested-class method body must stay out of scope; "
814
+ f"got: {issues!r}"
815
+ )
816
+
817
+
818
+ def test_should_ignore_path_home_inside_standalone_nested_helper_function() -> None:
819
+ # A standalone nested function defined inside a test body is its own
820
+ # callable scope — it carries its own isolation contract and is not
821
+ # part of the test's direct execution path. Probes there must remain
822
+ # unattributed to the outer test (preserves existing scope boundary).
823
+ source = (
824
+ "from pathlib import Path\n"
825
+ "def test_outer() -> None:\n"
826
+ " def helper() -> Path:\n"
827
+ " return Path.home()\n"
828
+ " assert callable(helper)\n"
829
+ )
830
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
831
+ assert issues == []
832
+
833
+
834
+ def test_should_flag_expanduser_with_tilde_only_argument() -> None:
835
+ source = (
836
+ "import os\n"
837
+ "def test_reads_home() -> None:\n"
838
+ " target = os.path.expanduser('~')\n"
839
+ " assert target\n"
840
+ )
841
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
842
+ assert any("expanduser" in each_issue for each_issue in issues)
843
+
844
+
845
+ def test_should_flag_expanduser_with_named_user_tilde_argument() -> None:
846
+ source = (
847
+ "import os\n"
848
+ "def test_reads_other_home() -> None:\n"
849
+ " target = os.path.expanduser('~alice/.config')\n"
850
+ " assert target\n"
851
+ )
852
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
853
+ assert any("expanduser" in each_issue for each_issue in issues)
854
+
855
+
856
+ def test_should_not_flag_expanduser_with_relative_path_without_tilde() -> None:
857
+ source = (
858
+ "import os\n"
859
+ "def test_resolves_relative() -> None:\n"
860
+ " target = os.path.expanduser('relative/path')\n"
861
+ " assert target\n"
862
+ )
863
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
864
+ assert issues == []
865
+
866
+
867
+ def test_should_not_flag_expanduser_with_non_constant_argument() -> None:
868
+ source = (
869
+ "import os\n"
870
+ "def test_resolves_dynamic(some_path) -> None:\n"
871
+ " target = os.path.expanduser(some_path)\n"
872
+ " assert target\n"
873
+ )
874
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
875
+ assert issues == []
876
+
877
+
878
+ def test_should_flag_from_os_import_path_expanduser() -> None:
879
+ source = (
880
+ "from os import path\n"
881
+ "def test_reads_dotfile() -> None:\n"
882
+ " target = path.expanduser('~/.config/x')\n"
883
+ " open(target).read()\n"
884
+ )
885
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
886
+ assert any("expanduser" in each_issue for each_issue in issues)
887
+
888
+
889
+ def test_should_flag_from_os_import_path_expandvars_home_var() -> None:
890
+ source = (
891
+ "from os import path\n"
892
+ "def test_expands_home() -> None:\n"
893
+ " target = path.expandvars('$HOME/.config/x')\n"
894
+ " open(target).read()\n"
895
+ )
896
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
897
+ assert any("expandvars" in each_issue for each_issue in issues)
898
+
899
+
900
+ def test_should_flag_from_os_import_path_under_alias_expanduser() -> None:
901
+ source = (
902
+ "from os import path as p\n"
903
+ "def test_reads_dotfile() -> None:\n"
904
+ " target = p.expanduser('~/.config/x')\n"
905
+ " open(target).read()\n"
906
+ )
907
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
908
+ assert any("expanduser" in each_issue for each_issue in issues)
909
+
910
+
911
+ def test_should_flag_expandvars_with_windows_percent_userprofile() -> None:
912
+ source = (
913
+ "import os\n"
914
+ "def test_expands_userprofile() -> None:\n"
915
+ " target = os.path.expandvars('%USERPROFILE%\\\\.cfg')\n"
916
+ " open(target).read()\n"
917
+ )
918
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
919
+ assert any("expandvars" in each_issue for each_issue in issues)
920
+
921
+
922
+ def test_should_flag_expandvars_with_windows_percent_temp() -> None:
923
+ source = (
924
+ "import os\n"
925
+ "def test_expands_temp() -> None:\n"
926
+ " target = os.path.expandvars('%TEMP%\\\\scratch')\n"
927
+ " open(target).read()\n"
928
+ )
929
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
930
+ assert any("expandvars" in each_issue for each_issue in issues)
931
+
932
+
933
+ def test_should_not_flag_expandvars_with_windows_percent_unrelated_var() -> None:
934
+ source = (
935
+ "import os\n"
936
+ "def test_expands_unrelated() -> None:\n"
937
+ " token = os.path.expandvars('%MY_APP_TOKEN%')\n"
938
+ " print(token)\n"
939
+ )
940
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
941
+ assert issues == []
942
+
943
+
944
+ def test_should_flag_bare_imported_expandvars_home_var() -> None:
945
+ source = (
946
+ "from os.path import expandvars\n"
947
+ "def test_expands_home() -> None:\n"
948
+ " target = expandvars('$HOME/.config/x')\n"
949
+ " open(target).read()\n"
950
+ )
951
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
952
+ assert any("expandvars" in each_issue for each_issue in issues)
953
+
954
+
955
+ def test_should_flag_from_pathlib_import_path_without_alias() -> None:
956
+ source = (
957
+ "from pathlib import Path\n"
958
+ "def test_reads_home() -> None:\n"
959
+ " home_dir = Path.home()\n"
960
+ " (home_dir / '.myapp').write_text('x')\n"
961
+ )
962
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
963
+ assert any("Path.home" in each_issue for each_issue in issues)
964
+
965
+
966
+ def test_should_flag_path_constructor_expanduser_method_call() -> None:
967
+ source = (
968
+ "from pathlib import Path\n"
969
+ "def test_reads_dotfile() -> None:\n"
970
+ " target = Path('~/x').expanduser()\n"
971
+ " target.read_text()\n"
972
+ )
973
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
974
+ assert any("expanduser" in each_issue for each_issue in issues)
975
+
976
+
977
+ def test_should_flag_aliased_path_constructor_expanduser_method_call() -> None:
978
+ source = (
979
+ "from pathlib import Path as P\n"
980
+ "def test_reads_dotfile() -> None:\n"
981
+ " target = P('~/x').expanduser()\n"
982
+ " target.read_text()\n"
983
+ )
984
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
985
+ assert any("expanduser" in each_issue for each_issue in issues)
986
+
987
+
988
+ def test_should_flag_pathlib_path_constructor_expanduser_method_call() -> None:
989
+ source = (
990
+ "import pathlib\n"
991
+ "def test_reads_dotfile() -> None:\n"
992
+ " target = pathlib.Path('~/x').expanduser()\n"
993
+ " target.read_text()\n"
994
+ )
995
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
996
+ assert any("expanduser" in each_issue for each_issue in issues)
997
+
998
+
999
+ def test_should_flag_expanduser_on_path_bound_local_variable() -> None:
1000
+ source = (
1001
+ "from pathlib import Path\n"
1002
+ "def test_reads_dotfile() -> None:\n"
1003
+ " candidate = Path('~/x')\n"
1004
+ " target = candidate.expanduser()\n"
1005
+ " target.read_text()\n"
1006
+ )
1007
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1008
+ assert any("expanduser" in each_issue for each_issue in issues)
1009
+
1010
+
1011
+ def test_should_flag_static_pathlib_path_expanduser_with_tilde_argument() -> None:
1012
+ source = (
1013
+ "import pathlib\n"
1014
+ "def test_reads_dotfile() -> None:\n"
1015
+ " target = pathlib.Path.expanduser('~/x')\n"
1016
+ " target.read_text()\n"
1017
+ )
1018
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1019
+ assert any("expanduser" in each_issue for each_issue in issues)
1020
+
1021
+
1022
+ def test_should_not_flag_static_pathlib_path_expanduser_with_dynamic_argument() -> None:
1023
+ source = (
1024
+ "import pathlib\n"
1025
+ "def test_resolves_dynamic(some_path) -> None:\n"
1026
+ " target = pathlib.Path.expanduser(some_path)\n"
1027
+ " target.read_text()\n"
1028
+ )
1029
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1030
+ assert issues == []
1031
+
1032
+
1033
+ def test_should_not_flag_static_pathlib_path_expanduser_with_tilde_free_argument() -> None:
1034
+ source = (
1035
+ "import pathlib\n"
1036
+ "def test_resolves_relative() -> None:\n"
1037
+ " target = pathlib.Path.expanduser('relative/path')\n"
1038
+ " target.read_text()\n"
1039
+ )
1040
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1041
+ assert issues == []
1042
+
1043
+
1044
+ def test_should_allow_path_constructor_expanduser_with_monkeypatch_fixture() -> None:
1045
+ source = (
1046
+ "from pathlib import Path\n"
1047
+ "def test_reads_dotfile(monkeypatch) -> None:\n"
1048
+ " monkeypatch.setenv('HOME', '/tmp/fake')\n"
1049
+ " target = Path('~/x').expanduser()\n"
1050
+ )
1051
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1052
+ assert issues == []
1053
+
1054
+
1055
+ def test_should_not_flag_expanduser_on_non_path_local_variable() -> None:
1056
+ source = (
1057
+ "def test_reads_dotfile(some_object) -> None:\n"
1058
+ " target = some_object.expanduser()\n"
1059
+ " print(target)\n"
1060
+ )
1061
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1062
+ assert issues == []
1063
+
1064
+
1065
+ def test_should_flag_tempfile_named_temporary_file_without_isolation() -> None:
1066
+ source = (
1067
+ "import tempfile\n"
1068
+ "def test_writes_named_temp() -> None:\n"
1069
+ " handle = tempfile.NamedTemporaryFile()\n"
1070
+ " handle.write(b'x')\n"
1071
+ )
1072
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1073
+ assert any("NamedTemporaryFile" in each_issue for each_issue in issues)
1074
+
1075
+
1076
+ def test_should_flag_tempfile_temporary_file_without_isolation() -> None:
1077
+ source = (
1078
+ "import tempfile\n"
1079
+ "def test_writes_temp() -> None:\n"
1080
+ " handle = tempfile.TemporaryFile()\n"
1081
+ " handle.write(b'x')\n"
1082
+ )
1083
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1084
+ assert any("TemporaryFile" in each_issue for each_issue in issues)
1085
+
1086
+
1087
+ def test_should_flag_tempfile_temporary_directory_without_isolation() -> None:
1088
+ source = (
1089
+ "import tempfile\n"
1090
+ "def test_makes_temp_dir() -> None:\n"
1091
+ " holder = tempfile.TemporaryDirectory()\n"
1092
+ " print(holder.name)\n"
1093
+ )
1094
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1095
+ assert any("TemporaryDirectory" in each_issue for each_issue in issues)
1096
+
1097
+
1098
+ def test_should_flag_tempfile_mktemp_without_isolation() -> None:
1099
+ source = (
1100
+ "import tempfile\n"
1101
+ "def test_resolves_temp_name() -> None:\n"
1102
+ " candidate = tempfile.mktemp()\n"
1103
+ " print(candidate)\n"
1104
+ )
1105
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1106
+ assert any("mktemp" in each_issue for each_issue in issues)
1107
+
1108
+
1109
+ def test_should_flag_bare_imported_tempfile_named_temporary_file() -> None:
1110
+ source = (
1111
+ "from tempfile import NamedTemporaryFile\n"
1112
+ "def test_writes_named_temp() -> None:\n"
1113
+ " handle = NamedTemporaryFile()\n"
1114
+ " handle.write(b'x')\n"
1115
+ )
1116
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1117
+ assert any("NamedTemporaryFile" in each_issue for each_issue in issues)
1118
+
1119
+
1120
+ def test_should_allow_tempfile_constructor_with_monkeypatch_fixture() -> None:
1121
+ source = (
1122
+ "import tempfile\n"
1123
+ "def test_writes_named_temp(monkeypatch) -> None:\n"
1124
+ " monkeypatch.setenv('TMPDIR', '/tmp/fake')\n"
1125
+ " handle = tempfile.NamedTemporaryFile()\n"
1126
+ )
1127
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1128
+ assert issues == []
1129
+
1130
+
1131
+ def test_should_not_flag_path_constructor_expanduser_with_tilde_free_argument() -> None:
1132
+ source = (
1133
+ "from pathlib import Path\n"
1134
+ "def test_resolves_absolute() -> None:\n"
1135
+ " target = Path('/tmp/x').expanduser()\n"
1136
+ " target.read_text()\n"
1137
+ )
1138
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1139
+ assert issues == []
1140
+
1141
+
1142
+ def test_should_not_flag_path_constructor_expanduser_with_dynamic_argument() -> None:
1143
+ source = (
1144
+ "from pathlib import Path\n"
1145
+ "def test_resolves_dynamic(some_path) -> None:\n"
1146
+ " target = Path(some_path).expanduser()\n"
1147
+ " target.read_text()\n"
1148
+ )
1149
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1150
+ assert issues == []
1151
+
1152
+
1153
+ def test_should_not_flag_path_bound_local_expanduser_with_tilde_free_argument() -> None:
1154
+ source = (
1155
+ "from pathlib import Path\n"
1156
+ "def test_resolves_absolute() -> None:\n"
1157
+ " candidate = Path('/tmp/x')\n"
1158
+ " target = candidate.expanduser()\n"
1159
+ " target.read_text()\n"
1160
+ )
1161
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1162
+ assert issues == []
1163
+
1164
+
1165
+ def test_should_flag_path_home_via_function_local_class_alias() -> None:
1166
+ source = (
1167
+ "from pathlib import Path\n"
1168
+ "def test_reads_home() -> None:\n"
1169
+ " path_class = Path\n"
1170
+ " home_dir = path_class.home()\n"
1171
+ " (home_dir / '.myapp').write_text('x')\n"
1172
+ )
1173
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1174
+ assert any("home" in each_issue.lower() for each_issue in issues)
1175
+
1176
+
1177
+ def test_should_flag_getenv_via_function_local_callable_alias() -> None:
1178
+ source = (
1179
+ "import os\n"
1180
+ "def test_reads_home() -> None:\n"
1181
+ " read_env = os.getenv\n"
1182
+ " home = read_env('HOME')\n"
1183
+ " print(home)\n"
1184
+ )
1185
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1186
+ assert any("HOME" in each_issue for each_issue in issues)
1187
+
1188
+
1189
+ def test_should_not_flag_getenv_function_local_alias_for_unrelated_var() -> None:
1190
+ source = (
1191
+ "import os\n"
1192
+ "def test_reads_token() -> None:\n"
1193
+ " read_env = os.getenv\n"
1194
+ " token = read_env('MY_APP_TOKEN')\n"
1195
+ " print(token)\n"
1196
+ )
1197
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1198
+ assert issues == []
1199
+
1200
+
1201
+ def test_should_flag_expanduser_via_function_local_os_path_module_alias() -> None:
1202
+ source = (
1203
+ "import os\n"
1204
+ "def test_reads_dotfile() -> None:\n"
1205
+ " path_module = os.path\n"
1206
+ " target = path_module.expanduser('~/.config/x')\n"
1207
+ " open(target).read()\n"
1208
+ )
1209
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1210
+ assert any("expanduser" in each_issue for each_issue in issues)
1211
+
1212
+
1213
+ def test_should_flag_mkdtemp_via_function_local_tempfile_module_alias() -> None:
1214
+ source = (
1215
+ "import tempfile\n"
1216
+ "def test_makes_temp_dir() -> None:\n"
1217
+ " temp_module = tempfile\n"
1218
+ " holder = temp_module.mkdtemp()\n"
1219
+ " print(holder)\n"
1220
+ )
1221
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1222
+ assert any("mkdtemp" in each_issue for each_issue in issues)
1223
+
1224
+
1225
+ def test_should_flag_path_home_via_function_local_pathlib_module_alias() -> None:
1226
+ source = (
1227
+ "import pathlib\n"
1228
+ "def test_reads_home() -> None:\n"
1229
+ " pathlib_module = pathlib\n"
1230
+ " home_dir = pathlib_module.Path.home()\n"
1231
+ " (home_dir / '.myapp').write_text('x')\n"
1232
+ )
1233
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1234
+ assert any("home" in each_issue.lower() for each_issue in issues)
1235
+
1236
+
1237
+ def test_should_not_flag_local_class_alias_leaking_from_a_different_test() -> None:
1238
+ source = (
1239
+ "from pathlib import Path\n"
1240
+ "def test_a() -> None:\n"
1241
+ " path_class = Path\n"
1242
+ " path_class.home()\n"
1243
+ "def test_b(path_class) -> None:\n"
1244
+ " path_class.home()\n"
1245
+ )
1246
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1247
+ assert any("test_a" in each_issue for each_issue in issues)
1248
+ assert not any("test_b" in each_issue for each_issue in issues)
1249
+
1250
+
1251
+ def test_should_not_flag_sibling_test_using_name_of_class_body_aliased_import() -> None:
1252
+ """A probe alias imported inside one test class body binds only inside that
1253
+ class scope. A sibling top-level test that takes the same name as a
1254
+ parameter must not inherit the alias, so its dotted call on that name does
1255
+ not surface a HOME/TMP isolation finding."""
1256
+ source = (
1257
+ "class TestAlpha:\n"
1258
+ " import tempfile as t\n"
1259
+ " def test_alpha_probe(self) -> None:\n"
1260
+ " assert self.t is not None\n"
1261
+ "def test_sibling(t) -> None:\n"
1262
+ " t.mkdtemp()\n"
1263
+ )
1264
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1265
+ assert not any("test_sibling" in each_issue for each_issue in issues), (
1266
+ "a class-body aliased import must not leak into the module-wide alias "
1267
+ f"map and flag a sibling test, got: {issues!r}"
1268
+ )
1269
+
1270
+
1271
+ def test_should_flag_tempfile_gettempdirb_without_isolation() -> None:
1272
+ source = (
1273
+ "import tempfile\n"
1274
+ "def test_resolves_temp_bytes() -> None:\n"
1275
+ " base = tempfile.gettempdirb()\n"
1276
+ " print(base)\n"
1277
+ )
1278
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1279
+ assert any("gettempdirb" in each_issue for each_issue in issues)
1280
+
1281
+
1282
+ def test_should_flag_tempfile_spooled_temporary_file_without_isolation() -> None:
1283
+ source = (
1284
+ "import tempfile\n"
1285
+ "def test_writes_spooled_temp() -> None:\n"
1286
+ " handle = tempfile.SpooledTemporaryFile()\n"
1287
+ " handle.write(b'x')\n"
1288
+ )
1289
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1290
+ assert any("SpooledTemporaryFile" in each_issue for each_issue in issues)
1291
+
1292
+
1293
+ def test_should_flag_bare_imported_tempfile_spooled_temporary_file() -> None:
1294
+ source = (
1295
+ "from tempfile import SpooledTemporaryFile\n"
1296
+ "def test_writes_spooled_temp() -> None:\n"
1297
+ " handle = SpooledTemporaryFile()\n"
1298
+ " handle.write(b'x')\n"
1299
+ )
1300
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1301
+ assert any("SpooledTemporaryFile" in each_issue for each_issue in issues)
1302
+
1303
+
1304
+ def test_should_not_flag_named_temporary_file_with_explicit_dir() -> None:
1305
+ source = (
1306
+ "import tempfile\n"
1307
+ "def test_writes_named_temp(tmp_path) -> None:\n"
1308
+ " handle = tempfile.NamedTemporaryFile(dir=tmp_path)\n"
1309
+ " handle.write(b'x')\n"
1310
+ )
1311
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1312
+ assert issues == []
1313
+
1314
+
1315
+ def test_should_not_flag_mkdtemp_with_explicit_dir() -> None:
1316
+ source = (
1317
+ "import tempfile\n"
1318
+ "def test_makes_temp_dir(tmp_path) -> None:\n"
1319
+ " holder = tempfile.mkdtemp(dir=tmp_path)\n"
1320
+ " print(holder)\n"
1321
+ )
1322
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1323
+ assert issues == []
1324
+
1325
+
1326
+ def test_should_flag_named_temporary_file_without_dir() -> None:
1327
+ source = (
1328
+ "import tempfile\n"
1329
+ "def test_writes_named_temp() -> None:\n"
1330
+ " handle = tempfile.NamedTemporaryFile()\n"
1331
+ " handle.write(b'x')\n"
1332
+ )
1333
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1334
+ assert any("NamedTemporaryFile" in each_issue for each_issue in issues)
1335
+
1336
+
1337
+ def test_should_flag_named_temporary_file_with_dir_none() -> None:
1338
+ source = (
1339
+ "import tempfile\n"
1340
+ "def test_writes_named_temp() -> None:\n"
1341
+ " handle = tempfile.NamedTemporaryFile(dir=None)\n"
1342
+ " handle.write(b'x')\n"
1343
+ )
1344
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1345
+ assert any("NamedTemporaryFile" in each_issue for each_issue in issues)
1346
+
1347
+
1348
+ def test_should_flag_mkdtemp_with_dir_getenv_tmpdir() -> None:
1349
+ source = (
1350
+ "import os\n"
1351
+ "import tempfile\n"
1352
+ "def test_makes_temp_dir() -> None:\n"
1353
+ " holder = tempfile.mkdtemp(dir=os.getenv('TMPDIR'))\n"
1354
+ " print(holder)\n"
1355
+ )
1356
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1357
+ assert any("mkdtemp" in each_issue for each_issue in issues)
1358
+
1359
+
1360
+ def test_should_flag_named_temporary_file_with_dir_gettempdir() -> None:
1361
+ source = (
1362
+ "import tempfile\n"
1363
+ "def test_writes_named_temp() -> None:\n"
1364
+ " handle = tempfile.NamedTemporaryFile(dir=tempfile.gettempdir())\n"
1365
+ " handle.write(b'x')\n"
1366
+ )
1367
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1368
+ assert any("NamedTemporaryFile" in each_issue for each_issue in issues)
1369
+
1370
+
1371
+ def test_should_flag_named_temporary_file_with_dir_environ_subscript_tmp() -> None:
1372
+ source = (
1373
+ "import os\n"
1374
+ "import tempfile\n"
1375
+ "def test_writes_named_temp() -> None:\n"
1376
+ " handle = tempfile.NamedTemporaryFile(dir=os.environ['TMP'])\n"
1377
+ " handle.write(b'x')\n"
1378
+ )
1379
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1380
+ assert any("NamedTemporaryFile" in each_issue for each_issue in issues)
1381
+
1382
+
1383
+ def test_should_not_flag_named_temporary_file_with_dir_str_tmp_path() -> None:
1384
+ source = (
1385
+ "import tempfile\n"
1386
+ "def test_writes_named_temp(tmp_path) -> None:\n"
1387
+ " handle = tempfile.NamedTemporaryFile(dir=str(tmp_path))\n"
1388
+ " handle.write(b'x')\n"
1389
+ )
1390
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1391
+ assert issues == []
1392
+
1393
+
1394
+ def test_should_not_flag_mkdtemp_with_dir_str_tmp_path() -> None:
1395
+ source = (
1396
+ "import tempfile\n"
1397
+ "def test_makes_temp_dir(tmp_path) -> None:\n"
1398
+ " holder = tempfile.mkdtemp(dir=str(tmp_path))\n"
1399
+ " print(holder)\n"
1400
+ )
1401
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1402
+ assert issues == []
1403
+
1404
+
1405
+ def test_should_still_flag_gettempdir_when_factory_dir_exemption_active() -> None:
1406
+ source = (
1407
+ "import tempfile\n"
1408
+ "def test_reads_shared_temp() -> None:\n"
1409
+ " base = tempfile.gettempdir()\n"
1410
+ " print(base)\n"
1411
+ )
1412
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1413
+ assert any("gettempdir" in each_issue for each_issue in issues)
1414
+
1415
+
1416
+ def test_should_allow_home_probe_with_usefixtures_monkeypatch_decorator() -> None:
1417
+ source = (
1418
+ "import os\n"
1419
+ "import pytest\n"
1420
+ "@pytest.mark.usefixtures('monkeypatch')\n"
1421
+ "def test_reads_home() -> None:\n"
1422
+ " home = os.environ['HOME']\n"
1423
+ " print(home)\n"
1424
+ )
1425
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1426
+ assert issues == []
1427
+
1428
+
1429
+ def test_should_allow_path_home_probe_with_usefixtures_monkeypatch_decorator() -> None:
1430
+ source = (
1431
+ "from pathlib import Path\n"
1432
+ "import pytest\n"
1433
+ "@pytest.mark.usefixtures('monkeypatch')\n"
1434
+ "def test_writes_dotfile() -> None:\n"
1435
+ " home_dir = Path.home()\n"
1436
+ " (home_dir / '.myapp').write_text('x')\n"
1437
+ )
1438
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1439
+ assert issues == []
1440
+
1441
+
1442
+ def test_should_allow_home_probe_with_bare_mark_usefixtures_monkeypatch_decorator() -> None:
1443
+ source = (
1444
+ "import os\n"
1445
+ "from pytest import mark\n"
1446
+ "@mark.usefixtures('monkeypatch')\n"
1447
+ "def test_reads_home() -> None:\n"
1448
+ " home = os.environ['HOME']\n"
1449
+ " print(home)\n"
1450
+ )
1451
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1452
+ assert issues == []
1453
+
1454
+
1455
+ def test_should_allow_home_probe_with_usefixtures_monkeypatch_among_other_fixtures() -> None:
1456
+ source = (
1457
+ "import os\n"
1458
+ "import pytest\n"
1459
+ "@pytest.mark.usefixtures('tmp_path', 'monkeypatch')\n"
1460
+ "def test_reads_home() -> None:\n"
1461
+ " home = os.environ['HOME']\n"
1462
+ " print(home)\n"
1463
+ )
1464
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1465
+ assert issues == []
1466
+
1467
+
1468
+ def test_should_flag_home_probe_with_usefixtures_lacking_monkeypatch() -> None:
1469
+ source = (
1470
+ "import os\n"
1471
+ "import pytest\n"
1472
+ "@pytest.mark.usefixtures('tmp_path')\n"
1473
+ "def test_reads_home() -> None:\n"
1474
+ " home = os.environ['HOME']\n"
1475
+ " print(home)\n"
1476
+ )
1477
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1478
+ assert any("HOME" in each_issue for each_issue in issues)
1479
+
1480
+
1481
+ def test_should_flag_home_probe_with_unrelated_marker_decorator() -> None:
1482
+ source = (
1483
+ "import os\n"
1484
+ "import pytest\n"
1485
+ "@pytest.mark.parametrize('value', [1, 2])\n"
1486
+ "def test_reads_home(value) -> None:\n"
1487
+ " home = os.environ['HOME']\n"
1488
+ " print(home, value)\n"
1489
+ )
1490
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1491
+ assert any("HOME" in each_issue for each_issue in issues)
1492
+
1493
+
1494
+ def test_should_order_mixed_probe_types_by_source_line() -> None:
1495
+ source = (
1496
+ "import os\n"
1497
+ "from pathlib import Path\n"
1498
+ "import tempfile\n"
1499
+ "def test_many_probe_kinds() -> None:\n"
1500
+ " home = os.environ['HOME']\n"
1501
+ " base = tempfile.mkdtemp()\n"
1502
+ " root = Path.home()\n"
1503
+ " target = os.path.expanduser('~/x')\n"
1504
+ " print(home, base, root, target)\n"
1505
+ )
1506
+ issues = check_tests_use_isolated_filesystem_paths(source, TEST_FILE_PATH)
1507
+ reported_line_numbers = [
1508
+ int(each_issue.split(":", maxsplit=1)[0].removeprefix("Line ").strip())
1509
+ for each_issue in issues
1510
+ ]
1511
+ assert reported_line_numbers == sorted(reported_line_numbers)
1512
+ assert reported_line_numbers == [5, 6, 7, 8]