claude-dev-env 1.69.2 → 1.71.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.
@@ -0,0 +1,623 @@
1
+ """Tests for claude_md_orphan_file_blocker hook."""
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
+
13
+ import claude_md_orphan_file_blocker as blocker_module
14
+ from claude_md_orphan_file_blocker import (
15
+ find_missing_filenames,
16
+ find_referenced_filenames,
17
+ )
18
+ from code_rules_annotations_length import check_unused_known_pytest_fixture_parameters
19
+ from code_rules_naming_collection import check_collection_prefix
20
+
21
+ from hooks_constants.claude_md_orphan_file_blocker_constants import (
22
+ ORPHAN_FILE_ADDITIONAL_CONTEXT,
23
+ ORPHAN_FILE_MESSAGE_TEMPLATE,
24
+ )
25
+
26
+ HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "claude_md_orphan_file_blocker.py")
27
+
28
+ REPO_ROOT = Path(__file__).resolve().parents[4]
29
+
30
+ TABLE_WITH_PRESENT_FILE = (
31
+ "# example\n\n| File | What it does |\n|---|---|\n| `present_module.py` | Does a thing |\n"
32
+ )
33
+
34
+ TABLE_WITH_ABSENT_FILE = (
35
+ "# example\n\n| File | What it does |\n|---|---|\n| `reviewer_specs.py` | Does a thing |\n"
36
+ )
37
+
38
+ TABLE_WITH_ABSENT_README = (
39
+ "# example\n\n| Document | Purpose |\n|---|---|\n| `README.md` | Overview |\n"
40
+ )
41
+
42
+ TABLE_WITH_SLASH_COMMAND_AND_SUBDIR = (
43
+ "# example\n\n"
44
+ "| Entry | Description |\n"
45
+ "|---|---|\n"
46
+ "| `/commit` | Slash command |\n"
47
+ "| `scripts/` | A subdirectory |\n"
48
+ "| Plain prose, no backticks | Not a file |\n"
49
+ )
50
+
51
+
52
+ class _RunHook:
53
+ """Helper to test the hook via subprocess, mirroring the sibling test style."""
54
+
55
+ def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
56
+ payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
57
+ return subprocess.run(
58
+ [sys.executable, HOOK_SCRIPT_PATH],
59
+ input=payload,
60
+ capture_output=True,
61
+ text=True,
62
+ check=False,
63
+ )
64
+
65
+
66
+ _run_hook = _RunHook()
67
+
68
+
69
+ def _isolated_claude_md_path(tmp_path: Path) -> Path:
70
+ """Return a CLAUDE.md path nested in a dedicated empty directory.
71
+
72
+ Nesting the CLAUDE.md inside a child of tmp_path keeps the hook's scan root
73
+ (the CLAUDE.md directory's parent) controlled, so a sibling test's temp
74
+ content never resolves a filename the case expects to be absent.
75
+ """
76
+ isolated_directory = tmp_path / "package_directory"
77
+ isolated_directory.mkdir()
78
+ return isolated_directory / "CLAUDE.md"
79
+
80
+
81
+ def test_blocks_write_naming_absent_python_file(tmp_path: Path):
82
+ claude_md_path = _isolated_claude_md_path(tmp_path)
83
+ result = _run_hook(
84
+ "Write",
85
+ {
86
+ "file_path": str(claude_md_path),
87
+ "content": TABLE_WITH_ABSENT_FILE,
88
+ },
89
+ )
90
+ assert result.returncode == 0
91
+ output = json.loads(result.stdout)
92
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
93
+ assert "reviewer_specs.py" in output["hookSpecificOutput"]["permissionDecisionReason"]
94
+
95
+
96
+ def test_blocks_write_naming_absent_markdown_file(tmp_path: Path):
97
+ claude_md_path = _isolated_claude_md_path(tmp_path)
98
+ result = _run_hook(
99
+ "Write",
100
+ {
101
+ "file_path": str(claude_md_path),
102
+ "content": TABLE_WITH_ABSENT_README,
103
+ },
104
+ )
105
+ assert result.returncode == 0
106
+ output = json.loads(result.stdout)
107
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
108
+ assert "README.md" in output["hookSpecificOutput"]["permissionDecisionReason"]
109
+
110
+
111
+ def test_allows_write_when_referenced_file_present(tmp_path: Path):
112
+ (tmp_path / "present_module.py").write_text("x = 1\n", encoding="utf-8")
113
+ claude_md_path = tmp_path / "CLAUDE.md"
114
+ result = _run_hook(
115
+ "Write",
116
+ {
117
+ "file_path": str(claude_md_path),
118
+ "content": TABLE_WITH_PRESENT_FILE,
119
+ },
120
+ )
121
+ assert result.returncode == 0
122
+ assert result.stdout == ""
123
+
124
+
125
+ def test_allows_non_claude_md_target(tmp_path: Path):
126
+ other_path = tmp_path / "README.md"
127
+ result = _run_hook(
128
+ "Write",
129
+ {
130
+ "file_path": str(other_path),
131
+ "content": TABLE_WITH_ABSENT_FILE,
132
+ },
133
+ )
134
+ assert result.returncode == 0
135
+ assert result.stdout == ""
136
+
137
+
138
+ def test_allows_slash_commands_subdirs_and_prose(tmp_path: Path):
139
+ claude_md_path = tmp_path / "CLAUDE.md"
140
+ result = _run_hook(
141
+ "Write",
142
+ {
143
+ "file_path": str(claude_md_path),
144
+ "content": TABLE_WITH_SLASH_COMMAND_AND_SUBDIR,
145
+ },
146
+ )
147
+ assert result.returncode == 0
148
+ assert result.stdout == ""
149
+
150
+
151
+ def test_blocks_via_edit_new_string(tmp_path: Path):
152
+ claude_md_path = _isolated_claude_md_path(tmp_path)
153
+ result = _run_hook(
154
+ "Edit",
155
+ {
156
+ "file_path": str(claude_md_path),
157
+ "old_string": "| `old.py` | row |",
158
+ "new_string": TABLE_WITH_ABSENT_FILE,
159
+ },
160
+ )
161
+ assert result.returncode == 0
162
+ output = json.loads(result.stdout)
163
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
164
+ assert "reviewer_specs.py" in output["hookSpecificOutput"]["permissionDecisionReason"]
165
+
166
+
167
+ def test_blocks_via_multiedit_new_string(tmp_path: Path):
168
+ claude_md_path = _isolated_claude_md_path(tmp_path)
169
+ (claude_md_path.parent / "present_module.py").write_text("x = 1\n", encoding="utf-8")
170
+ result = _run_hook(
171
+ "MultiEdit",
172
+ {
173
+ "file_path": str(claude_md_path),
174
+ "edits": [
175
+ {"old_string": "a", "new_string": TABLE_WITH_PRESENT_FILE},
176
+ {"old_string": "b", "new_string": TABLE_WITH_ABSENT_FILE},
177
+ ],
178
+ },
179
+ )
180
+ assert result.returncode == 0
181
+ output = json.loads(result.stdout)
182
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
183
+ assert "reviewer_specs.py" in output["hookSpecificOutput"]["permissionDecisionReason"]
184
+
185
+
186
+ def test_block_payload_carries_directory_and_system_message(tmp_path: Path):
187
+ claude_md_path = _isolated_claude_md_path(tmp_path)
188
+ result = _run_hook(
189
+ "Write",
190
+ {
191
+ "file_path": str(claude_md_path),
192
+ "content": TABLE_WITH_ABSENT_FILE,
193
+ },
194
+ )
195
+ assert result.returncode == 0
196
+ output = json.loads(result.stdout)
197
+ assert output["suppressOutput"] is True
198
+ assert isinstance(output["systemMessage"], str)
199
+ assert len(output["systemMessage"]) > 0
200
+ assert str(claude_md_path.parent.resolve()) in output["hookSpecificOutput"]["permissionDecisionReason"]
201
+
202
+
203
+ def test_allows_file_present_in_subdirectory(tmp_path: Path):
204
+ workflows_dir = tmp_path / "workflows"
205
+ workflows_dir.mkdir()
206
+ (workflows_dir / "pr-check.yml").write_text("name: ci\n", encoding="utf-8")
207
+ claude_md_path = tmp_path / "CLAUDE.md"
208
+ content = (
209
+ "# example\n\n"
210
+ "| File | Trigger |\n"
211
+ "|---|---|\n"
212
+ "| `pr-check.yml` | PR opened |\n"
213
+ )
214
+ result = _run_hook(
215
+ "Write",
216
+ {
217
+ "file_path": str(claude_md_path),
218
+ "content": content,
219
+ },
220
+ )
221
+ assert result.returncode == 0
222
+ assert result.stdout == ""
223
+
224
+
225
+ def test_allows_table_declaring_relative_path_source(tmp_path: Path):
226
+ claude_md_path = tmp_path / "CLAUDE.md"
227
+ content = (
228
+ "# example\n\n"
229
+ "## Shared artifacts (referenced by path, not copied)\n\n"
230
+ "The skill references shared scripts from `../_shared/pr-loop/scripts/`:\n\n"
231
+ "| Script | Role |\n"
232
+ "|---|---|\n"
233
+ "| `preflight.py` | Pre-flight check |\n"
234
+ )
235
+ result = _run_hook(
236
+ "Write",
237
+ {
238
+ "file_path": str(claude_md_path),
239
+ "content": content,
240
+ },
241
+ )
242
+ assert result.returncode == 0
243
+ assert result.stdout == ""
244
+
245
+
246
+ def test_separator_row_is_skipped(tmp_path: Path):
247
+ claude_md_path = tmp_path / "CLAUDE.md"
248
+ content = "| File | Note |\n|---|---|\n"
249
+ result = _run_hook(
250
+ "Write",
251
+ {
252
+ "file_path": str(claude_md_path),
253
+ "content": content,
254
+ },
255
+ )
256
+ assert result.returncode == 0
257
+ assert result.stdout == ""
258
+
259
+
260
+ def test_relative_path_table_does_not_exempt_sibling_local_table(tmp_path: Path):
261
+ claude_md_path = _isolated_claude_md_path(tmp_path)
262
+ content = (
263
+ "# example\n\n"
264
+ "## Local files\n\n"
265
+ "| File | Note |\n"
266
+ "|---|---|\n"
267
+ "| `reviewer_specs.py` | Does a thing |\n\n"
268
+ "## Shared artifacts\n\n"
269
+ "Referenced from `../_shared/scripts/`:\n\n"
270
+ "| Script | Role |\n"
271
+ "|---|---|\n"
272
+ "| `preflight.py` | Pre-flight check |\n"
273
+ )
274
+ result = _run_hook(
275
+ "Write",
276
+ {
277
+ "file_path": str(claude_md_path),
278
+ "content": content,
279
+ },
280
+ )
281
+ assert result.returncode == 0
282
+ output = json.loads(result.stdout)
283
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
284
+ assert "reviewer_specs.py" in output["hookSpecificOutput"]["permissionDecisionReason"]
285
+ assert "preflight.py" not in output["hookSpecificOutput"]["permissionDecisionReason"]
286
+
287
+
288
+ def test_relative_path_prose_above_its_own_table_exempts_that_table(tmp_path: Path):
289
+ claude_md_path = _isolated_claude_md_path(tmp_path)
290
+ content = (
291
+ "# example\n\n"
292
+ "## Shared artifacts\n\n"
293
+ "Referenced from `../_shared/scripts/`:\n\n"
294
+ "| Script | Role |\n"
295
+ "|---|---|\n"
296
+ "| `preflight.py` | Pre-flight check |\n"
297
+ )
298
+ result = _run_hook(
299
+ "Write",
300
+ {
301
+ "file_path": str(claude_md_path),
302
+ "content": content,
303
+ },
304
+ )
305
+ assert result.returncode == 0
306
+ assert result.stdout == ""
307
+
308
+
309
+ def test_edit_to_relative_path_sourced_table_is_allowed(tmp_path: Path):
310
+ claude_md_path = _isolated_claude_md_path(tmp_path)
311
+ claude_md_path.write_text(
312
+ "# example\n\n"
313
+ "## Shared artifacts\n\n"
314
+ "Referenced from `../_shared/scripts/`:\n\n"
315
+ "| Script | Role |\n"
316
+ "|---|---|\n"
317
+ "| `code_rules_gate.py` | Gate |\n",
318
+ encoding="utf-8",
319
+ )
320
+ result = _run_hook(
321
+ "Edit",
322
+ {
323
+ "file_path": str(claude_md_path),
324
+ "old_string": "| `code_rules_gate.py` | Gate |",
325
+ "new_string": (
326
+ "| `code_rules_gate.py` | Gate |\n"
327
+ "| `preflight.py` | Pre-flight gate that runs before the audit |"
328
+ ),
329
+ },
330
+ )
331
+ assert result.returncode == 0
332
+ assert result.stdout == ""
333
+
334
+
335
+ def test_edit_adding_orphan_to_non_exempt_file_is_denied(tmp_path: Path):
336
+ claude_md_path = _isolated_claude_md_path(tmp_path)
337
+ claude_md_path.write_text(
338
+ "# example\n\n"
339
+ "## Local files\n\n"
340
+ "| File | Note |\n"
341
+ "|---|---|\n"
342
+ "| `kept.py` | row |\n",
343
+ encoding="utf-8",
344
+ )
345
+ (claude_md_path.parent / "kept.py").write_text("x = 1\n", encoding="utf-8")
346
+ result = _run_hook(
347
+ "Edit",
348
+ {
349
+ "file_path": str(claude_md_path),
350
+ "old_string": "| `kept.py` | row |",
351
+ "new_string": (
352
+ "| `kept.py` | row |\n| `reviewer_specs.py` | Does a thing |"
353
+ ),
354
+ },
355
+ )
356
+ assert result.returncode == 0
357
+ output = json.loads(result.stdout)
358
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
359
+ assert "reviewer_specs.py" in output["hookSpecificOutput"]["permissionDecisionReason"]
360
+
361
+
362
+ def test_block_lines_yield_their_filenames_when_region_is_not_exempt():
363
+ content = (
364
+ "# example\n\n"
365
+ "## Local files\n\n"
366
+ "| File | Note |\n"
367
+ "|---|---|\n"
368
+ "| `alpha.py` | row |\n"
369
+ "| `beta.py` | row |\n"
370
+ )
371
+ assert find_referenced_filenames(content) == ["alpha.py", "beta.py"]
372
+
373
+
374
+ def test_block_lines_yield_nothing_when_region_declares_relative_source():
375
+ content = (
376
+ "# example\n\n"
377
+ "Referenced from `../_shared/scripts/`:\n\n"
378
+ "| Script | Role |\n"
379
+ "|---|---|\n"
380
+ "| `preflight.py` | Pre-flight check |\n"
381
+ )
382
+ assert find_referenced_filenames(content) == []
383
+
384
+
385
+ def test_distant_relative_prose_does_not_exempt_later_local_table():
386
+ content = (
387
+ "Intro: see ../shared/ for context.\n\n"
388
+ "## Local files\n\n"
389
+ "| File | Note |\n"
390
+ "|---|---|\n"
391
+ "| `alpha.py` | row |\n"
392
+ )
393
+ assert find_referenced_filenames(content) == ["alpha.py"]
394
+
395
+
396
+ def test_relative_prose_then_distant_local_table_is_denied(tmp_path: Path):
397
+ claude_md_path = _isolated_claude_md_path(tmp_path)
398
+ content = (
399
+ "# example\n\n"
400
+ "Intro: see `../_shared/scripts/` for context.\n\n"
401
+ "## Local files\n\n"
402
+ "| File | Note |\n"
403
+ "|---|---|\n"
404
+ "| `reviewer_specs.py` | Does a thing |\n"
405
+ )
406
+ result = _run_hook(
407
+ "Write",
408
+ {
409
+ "file_path": str(claude_md_path),
410
+ "content": content,
411
+ },
412
+ )
413
+ assert result.returncode == 0
414
+ output = json.loads(result.stdout)
415
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
416
+ assert "reviewer_specs.py" in output["hookSpecificOutput"]["permissionDecisionReason"]
417
+
418
+
419
+ def test_corrective_message_names_siblings_under_parent():
420
+ assert "sibling" in ORPHAN_FILE_MESSAGE_TEMPLATE
421
+ assert "sibling" in ORPHAN_FILE_ADDITIONAL_CONTEXT
422
+
423
+
424
+ def test_every_repo_claude_md_is_not_blocked():
425
+ all_claude_md_paths = sorted(REPO_ROOT.rglob("CLAUDE.md"))
426
+ assert all_claude_md_paths, "expected the repo to contain CLAUDE.md files"
427
+ all_offenders: list[str] = []
428
+ for each_path in all_claude_md_paths:
429
+ content = each_path.read_text(encoding="utf-8")
430
+ missing_filenames = find_missing_filenames(content, each_path.parent)
431
+ if missing_filenames:
432
+ all_offenders.append(f"{each_path}: {missing_filenames}")
433
+ assert not all_offenders, "\n".join(all_offenders)
434
+
435
+
436
+ def test_unrelated_edit_over_preexisting_orphan_row_is_allowed(tmp_path: Path):
437
+ claude_md_path = _isolated_claude_md_path(tmp_path)
438
+ (claude_md_path.parent / "kept.py").write_text("x = 1\n", encoding="utf-8")
439
+ claude_md_path.write_text(
440
+ "# example\n\n"
441
+ "A prose paragraph with a typoo to fix.\n\n"
442
+ "## Local files\n\n"
443
+ "| File | Note |\n"
444
+ "|---|---|\n"
445
+ "| `kept.py` | row |\n"
446
+ "| `already_orphan.py` | pre-existing orphan |\n",
447
+ encoding="utf-8",
448
+ )
449
+ result = _run_hook(
450
+ "Edit",
451
+ {
452
+ "file_path": str(claude_md_path),
453
+ "old_string": "A prose paragraph with a typoo to fix.",
454
+ "new_string": "A prose paragraph with a typo fixed.",
455
+ },
456
+ )
457
+ assert result.returncode == 0
458
+ assert result.stdout == ""
459
+
460
+
461
+ def test_multiedit_unrelated_change_over_preexisting_orphan_is_allowed(tmp_path: Path):
462
+ claude_md_path = _isolated_claude_md_path(tmp_path)
463
+ (claude_md_path.parent / "kept.py").write_text("x = 1\n", encoding="utf-8")
464
+ claude_md_path.write_text(
465
+ "# example\n\n"
466
+ "First prose paragraph.\n\n"
467
+ "## Local files\n\n"
468
+ "| File | Note |\n"
469
+ "|---|---|\n"
470
+ "| `kept.py` | row |\n"
471
+ "| `already_orphan.py` | pre-existing orphan |\n",
472
+ encoding="utf-8",
473
+ )
474
+ result = _run_hook(
475
+ "MultiEdit",
476
+ {
477
+ "file_path": str(claude_md_path),
478
+ "edits": [
479
+ {"old_string": "First prose paragraph.", "new_string": "Revised prose paragraph."},
480
+ ],
481
+ },
482
+ )
483
+ assert result.returncode == 0
484
+ assert result.stdout == ""
485
+
486
+
487
+ def test_fenced_example_table_row_is_skipped(tmp_path: Path):
488
+ claude_md_path = _isolated_claude_md_path(tmp_path)
489
+ content = (
490
+ "# example\n\n"
491
+ "An example table you might write:\n\n"
492
+ "```\n"
493
+ "| File | Note |\n"
494
+ "|---|---|\n"
495
+ "| `ghostfile.py` | example row |\n"
496
+ "```\n"
497
+ )
498
+ result = _run_hook(
499
+ "Write",
500
+ {
501
+ "file_path": str(claude_md_path),
502
+ "content": content,
503
+ },
504
+ )
505
+ assert result.returncode == 0
506
+ assert result.stdout == ""
507
+
508
+
509
+ def test_fenced_table_row_does_not_exempt_a_later_real_row():
510
+ content = (
511
+ "# example\n\n"
512
+ "```\n"
513
+ "| File | Note |\n"
514
+ "|---|---|\n"
515
+ "| `ghostfile.py` | fenced example |\n"
516
+ "```\n\n"
517
+ "## Local files\n\n"
518
+ "| File | Note |\n"
519
+ "|---|---|\n"
520
+ "| `reviewer_specs.py` | real row |\n"
521
+ )
522
+ assert find_referenced_filenames(content) == ["reviewer_specs.py"]
523
+
524
+
525
+ def test_tilde_fenced_example_table_row_is_skipped():
526
+ content = (
527
+ "# example\n\n"
528
+ "~~~\n"
529
+ "| File | Note |\n"
530
+ "|---|---|\n"
531
+ "| `ghostfile.py` | example row |\n"
532
+ "~~~\n"
533
+ )
534
+ assert find_referenced_filenames(content) == []
535
+
536
+
537
+ def test_file_present_only_past_the_scan_cap_is_not_missing(
538
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
539
+ ):
540
+ monkeypatch.setattr(blocker_module, "MAX_SUBTREE_FILES_SCANNED", 5)
541
+ package_directory = tmp_path / "package_directory"
542
+ package_directory.mkdir()
543
+ filler_directory = tmp_path / "filler"
544
+ filler_directory.mkdir()
545
+ for each_index in range(50):
546
+ (filler_directory / f"filler_{each_index}.py").write_text("x = 1\n", encoding="utf-8")
547
+ (package_directory / "real_target.py").write_text("x = 1\n", encoding="utf-8")
548
+ claude_md_path = package_directory / "CLAUDE.md"
549
+ content = (
550
+ "# example\n\n"
551
+ "| File | Note |\n"
552
+ "|---|---|\n"
553
+ "| `real_target.py` | a file that genuinely exists |\n"
554
+ )
555
+ first_missing = find_missing_filenames(content, claude_md_path.parent)
556
+ second_missing = find_missing_filenames(content, claude_md_path.parent)
557
+ assert first_missing == []
558
+ assert second_missing == []
559
+
560
+
561
+ def test_oserror_during_subtree_walk_fails_open(
562
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
563
+ ):
564
+ def _raise_oserror(self, pattern):
565
+ raise OSError("simulated unreadable directory")
566
+
567
+ monkeypatch.setattr(Path, "rglob", _raise_oserror)
568
+ claude_md_path = tmp_path / "CLAUDE.md"
569
+ content = (
570
+ "# example\n\n"
571
+ "| File | Note |\n"
572
+ "|---|---|\n"
573
+ "| `reviewer_specs.py` | absent |\n"
574
+ )
575
+ missing_filenames = find_missing_filenames(content, claude_md_path.parent)
576
+ assert missing_filenames == []
577
+
578
+
579
+ def test_no_referenced_filenames_performs_no_subtree_walk(
580
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
581
+ ):
582
+ def _fail_if_walked(self, pattern):
583
+ raise AssertionError("rglob walked the subtree with no filenames to verify")
584
+
585
+ monkeypatch.setattr(Path, "rglob", _fail_if_walked)
586
+ claude_md_path = tmp_path / "CLAUDE.md"
587
+ content = (
588
+ "# example\n\n"
589
+ "| Task | Command |\n"
590
+ "|---|---|\n"
591
+ "| Run the tests | npm test |\n"
592
+ )
593
+ missing_filenames = find_missing_filenames(content, claude_md_path.parent)
594
+ assert missing_filenames == []
595
+
596
+
597
+ def test_noise_directories_are_excluded_from_the_walk(tmp_path: Path):
598
+ package_directory = tmp_path / "package_directory"
599
+ package_directory.mkdir()
600
+ for each_noise_name in (".git", "__pycache__", "node_modules", ".pytest_cache", ".ruff_cache"):
601
+ noise_directory = tmp_path / each_noise_name
602
+ noise_directory.mkdir()
603
+ (noise_directory / "buried_target.py").write_text("x = 1\n", encoding="utf-8")
604
+ claude_md_path = package_directory / "CLAUDE.md"
605
+ content = (
606
+ "# example\n\n"
607
+ "| File | Note |\n"
608
+ "|---|---|\n"
609
+ "| `buried_target.py` | only present inside noise directories |\n"
610
+ )
611
+ missing_filenames = find_missing_filenames(content, claude_md_path.parent)
612
+ assert missing_filenames == ["buried_target.py"]
613
+
614
+
615
+ def test_blocker_module_has_no_collection_parameter_naming_violations():
616
+ blocker_source = Path(HOOK_SCRIPT_PATH).read_text(encoding="utf-8")
617
+ assert check_collection_prefix(blocker_source, HOOK_SCRIPT_PATH) == []
618
+
619
+
620
+ def test_test_module_has_no_unused_pytest_fixture_parameters():
621
+ test_file_path = str(Path(__file__).resolve())
622
+ test_source = Path(test_file_path).read_text(encoding="utf-8")
623
+ assert check_unused_known_pytest_fixture_parameters(test_source, test_file_path) == []
@@ -7,7 +7,7 @@ Native git hooks that run outside the Claude Code lifecycle — invoked directly
7
7
  | File | Git hook | What it does |
8
8
  |---|---|---|
9
9
  | `pre_commit.py` | `pre-commit` | Runs the CODE_RULES gate (`precommit_code_rules_gate.py`) over staged changes; exits 1 when any staged file has a blocking violation |
10
- | `pre_push.py` | `pre-push` | Runs the verified-commit gate check before a push reaches the remote |
10
+ | `pre_push.py` | `pre-push` | Blocks a push that would land a non-`main` local branch onto remote `main` (or `master`), then runs the CODE_RULES gate over the commits about to be pushed |
11
11
  | `post_commit.py` | `post-commit` | Runs after a commit lands; performs any post-commit bookkeeping |
12
12
  | `gate_utils.py` | — | Shared helpers: resolves the gate script path, checks that the path is a safe regular file |
13
13
  | `test_config.py` | — | Test configuration helpers |
@@ -16,6 +16,8 @@ DEFAULT_REMOTE_BASE_REFERENCE: str = "origin/HEAD"
16
16
  ALL_ZEROS_OBJECT_NAME_CHARACTER: str = "0"
17
17
  STDIN_LINE_FIELD_COUNT: int = 4
18
18
  STDIN_REMOTE_OBJECT_FIELD_INDEX: int = 3
19
+ LOCAL_REFERENCE_FIELD_INDEX: int = 0
20
+ REMOTE_REFERENCE_FIELD_INDEX: int = 2
19
21
  GATE_PATH_OVERRIDE_ENV_VAR: str = "CODE_RULES_GATE_PATH"
20
22
  CLAUDE_HOME_ENV_VAR: str = "CLAUDE_HOME"
21
23
  CLAUDE_HOME_DEFAULT_SUBDIRECTORY: str = ".claude"
@@ -46,3 +48,14 @@ NO_PARSEABLE_STDIN_LINES_MESSAGE: str = (
46
48
  "claude-dev-env pre-push: no parseable stdin lines; aborting"
47
49
  )
48
50
  NO_PARSEABLE_STDIN_LINES_SENTINEL: str = "__no_parseable_stdin_lines__"
51
+ LOCAL_BRANCH_REFERENCE_PREFIX: str = "refs/heads/"
52
+ ALL_PROTECTED_BRANCH_PUSH_NAMES: tuple[str, ...] = ("main", "master")
53
+ PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE: int = 1
54
+ PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE: str = (
55
+ "claude-dev-env pre-push: blocked a push of local branch {local_branch!r} "
56
+ "onto protected remote branch {remote_branch!r}.\n"
57
+ "A local branch that tracks origin/{remote_branch}, with push.default=upstream, "
58
+ "resolves a bare 'git push' to {remote_branch}.\n"
59
+ "To push the feature branch to its own ref, name the destination: "
60
+ "git push origin {local_branch}:refs/heads/{local_branch}"
61
+ )