claude-dev-env 1.25.2 → 1.26.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 (105) hide show
  1. package/CLAUDE.md +6 -0
  2. package/agents/clean-coder.md +1 -1
  3. package/docs/CODE_RULES.md +3 -1
  4. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +54 -0
  5. package/hooks/blocking/{code-rules-enforcer.py → code_rules_enforcer.py} +150 -5
  6. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
  7. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
  8. package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
  9. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
  10. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +181 -0
  11. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
  12. package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
  13. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  14. package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
  15. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
  16. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
  17. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
  18. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  19. package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
  20. package/hooks/blocking/test_pr_description_enforcer.py +8 -8
  21. package/hooks/blocking/test_tdd_enforcer.py +1 -1
  22. package/hooks/github-action/pre-push-review.yml +27 -0
  23. package/hooks/hooks.json +28 -28
  24. package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +27 -12
  25. package/hooks/lifecycle/test_config_change_guard.py +3 -3
  26. package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
  27. package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
  28. package/hooks/notification/notification_utils.py +56 -0
  29. package/hooks/notification/subagent_complete_notify.py +381 -0
  30. package/hooks/notification/test_attention_needed_notify.py +47 -0
  31. package/hooks/notification/test_claude_notification_handler.py +54 -0
  32. package/hooks/notification/test_notification_utils.py +45 -0
  33. package/hooks/notification/test_subagent_complete_notify.py +72 -0
  34. package/hooks/validators/README.md +5 -1
  35. package/hooks/validators/abbreviation_checks.py +1 -1
  36. package/hooks/validators/code_quality_checks.py +1 -1
  37. package/hooks/validators/config.py +5 -0
  38. package/hooks/validators/conftest.py +10 -0
  39. package/hooks/validators/exempt_paths.py +1 -1
  40. package/hooks/validators/git_checks.py +80 -0
  41. package/hooks/validators/magic_value_checks.py +2 -2
  42. package/hooks/validators/pr_reference_checks.py +1 -1
  43. package/hooks/validators/python_antipattern_checks.py +1 -1
  44. package/hooks/validators/run_all_validators.py +53 -105
  45. package/hooks/validators/security_checks.py +1 -1
  46. package/hooks/validators/test_abbreviation_checks.py +2 -2
  47. package/hooks/validators/test_code_quality_checks.py +2 -2
  48. package/hooks/validators/test_file_structure_checks.py +1 -1
  49. package/hooks/validators/test_git_checks.py +79 -13
  50. package/hooks/validators/test_health_check.py +1 -1
  51. package/hooks/validators/test_magic_value_checks.py +2 -2
  52. package/hooks/validators/test_mypy_integration.py +1 -1
  53. package/hooks/validators/test_output_formatter.py +3 -1
  54. package/hooks/validators/test_pr_reference_checks.py +2 -2
  55. package/hooks/validators/test_python_antipattern_checks.py +2 -2
  56. package/hooks/validators/test_python_style_checks.py +2 -4
  57. package/hooks/validators/test_react_checks.py +1 -1
  58. package/hooks/validators/test_ruff_integration.py +1 -1
  59. package/hooks/validators/test_run_all_validators.py +75 -43
  60. package/hooks/validators/test_run_all_validators_integration.py +14 -37
  61. package/hooks/validators/test_security_checks.py +2 -2
  62. package/hooks/validators/test_test_safety_checks.py +1 -1
  63. package/hooks/validators/test_todo_checks.py +2 -2
  64. package/hooks/validators/test_type_safety_checks.py +2 -2
  65. package/hooks/validators/test_useless_test_checks.py +2 -2
  66. package/hooks/validators/test_validator_base.py +1 -1
  67. package/hooks/validators/test_verify_paths.py +2 -4
  68. package/hooks/validators/todo_checks.py +1 -1
  69. package/hooks/validators/type_safety_checks.py +1 -1
  70. package/hooks/validators/useless_test_checks.py +1 -1
  71. package/package.json +1 -1
  72. package/rules/file-global-constants.md +71 -0
  73. package/rules/gh-body-file.md +1 -1
  74. package/rules/prompt-workflow-context-controls.md +48 -0
  75. package/scripts/sync_to_cursor/rules.py +2 -2
  76. package/scripts/tests/test_sync_to_cursor.py +2 -2
  77. package/skills/bugteam/CONSTRAINTS.md +37 -0
  78. package/skills/bugteam/EXAMPLES.md +64 -0
  79. package/skills/bugteam/PROMPTS.md +175 -0
  80. package/skills/bugteam/SKILL.md +204 -295
  81. package/skills/bugteam/SKILL_EVALS.md +346 -0
  82. package/skills/bugteam/scripts/README.md +37 -0
  83. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
  84. package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
  85. package/skills/rule-audit/SKILL.md +4 -4
  86. /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
  87. /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
  88. /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
  89. /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.py} +0 -0
  90. /package/hooks/blocking/{destructive-command-blocker.py → destructive_command_blocker.py} +0 -0
  91. /package/hooks/blocking/{gh-body-arg-blocker.py → gh_body_arg_blocker.py} +0 -0
  92. /package/hooks/blocking/{hedging-language-blocker.py → hedging_language_blocker.py} +0 -0
  93. /package/hooks/blocking/{pr-description-enforcer.py → pr_description_enforcer.py} +0 -0
  94. /package/hooks/blocking/{sensitive-file-protector.py → sensitive_file_protector.py} +0 -0
  95. /package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +0 -0
  96. /package/hooks/blocking/{test-preflight-check.py → test_preflight_check.py} +0 -0
  97. /package/hooks/blocking/{write-existing-file-blocker.py → write_existing_file_blocker.py} +0 -0
  98. /package/hooks/git-hooks/{post-commit.py → post_commit.py} +0 -0
  99. /package/hooks/lifecycle/{session-end-cleanup.py → session_end_cleanup.py} +0 -0
  100. /package/hooks/{rewrite-plugin-paths.py → rewrite_plugin_paths.py} +0 -0
  101. /package/hooks/session/{plugin-data-dir-cleanup.py → plugin_data_dir_cleanup.py} +0 -0
  102. /package/hooks/validation/{hook-format-validator.py → hook_format_validator.py} +0 -0
  103. /package/hooks/workflow/{auto-formatter.py → auto_formatter.py} +0 -0
  104. /package/hooks/workflow/{investigation-tracker-reset.py → investigation_tracker_reset.py} +0 -0
  105. /package/scripts/{sync-to-cursor.py → sync_to_cursor.py} +0 -0
@@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch
8
8
 
9
9
  import pytest
10
10
 
11
- from git_checks import (
11
+ from .git_checks import (
12
12
  Violation,
13
13
  check_single_commit_when_pr_exists,
14
14
  check_draft_pr_state,
@@ -22,7 +22,10 @@ class TestSingleCommitWhenPrExists:
22
22
  @patch("git_checks.subprocess.run")
23
23
  def test_no_pr_returns_empty(self, mock_run: MagicMock) -> None:
24
24
  """When no PR exists, check should return empty list."""
25
- mock_run.return_value = MagicMock(returncode=0, stdout="[]", stderr="")
25
+ mock_run.side_effect = [
26
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
27
+ MagicMock(returncode=0, stdout="[]", stderr=""),
28
+ ]
26
29
 
27
30
  violations = check_single_commit_when_pr_exists()
28
31
 
@@ -32,6 +35,7 @@ class TestSingleCommitWhenPrExists:
32
35
  def test_single_commit_ahead_passes(self, mock_run: MagicMock) -> None:
33
36
  """Exactly 1 commit ahead should pass."""
34
37
  mock_run.side_effect = [
38
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
35
39
  MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
36
40
  MagicMock(returncode=0, stdout="1", stderr=""),
37
41
  ]
@@ -44,6 +48,7 @@ class TestSingleCommitWhenPrExists:
44
48
  def test_zero_commits_ahead_fails(self, mock_run: MagicMock) -> None:
45
49
  """Zero commits ahead should fail."""
46
50
  mock_run.side_effect = [
51
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
47
52
  MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
48
53
  MagicMock(returncode=0, stdout="0", stderr=""),
49
54
  ]
@@ -60,6 +65,7 @@ class TestSingleCommitWhenPrExists:
60
65
  def test_multiple_commits_ahead_fails(self, mock_run: MagicMock) -> None:
61
66
  """More than 1 commit ahead should fail."""
62
67
  mock_run.side_effect = [
68
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
63
69
  MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
64
70
  MagicMock(returncode=0, stdout="3", stderr=""),
65
71
  ]
@@ -73,7 +79,10 @@ class TestSingleCommitWhenPrExists:
73
79
  @patch("git_checks.subprocess.run")
74
80
  def test_gh_cli_not_available_returns_empty(self, mock_run: MagicMock) -> None:
75
81
  """When gh CLI not available, should return empty (warning, not failure)."""
76
- mock_run.side_effect = FileNotFoundError("gh not found")
82
+ mock_run.side_effect = [
83
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
84
+ FileNotFoundError("gh not found"),
85
+ ]
77
86
 
78
87
  violations = check_single_commit_when_pr_exists()
79
88
 
@@ -83,6 +92,7 @@ class TestSingleCommitWhenPrExists:
83
92
  def test_git_not_available_returns_empty(self, mock_run: MagicMock) -> None:
84
93
  """When git not available, should return empty."""
85
94
  mock_run.side_effect = [
95
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
86
96
  MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
87
97
  FileNotFoundError("git not found"),
88
98
  ]
@@ -93,8 +103,9 @@ class TestSingleCommitWhenPrExists:
93
103
 
94
104
  @patch("git_checks.subprocess.run")
95
105
  def test_extracts_base_branch_from_pr_info(self, mock_run: MagicMock) -> None:
96
- """Should extract base branch name from gh pr list JSON output."""
106
+ """Should extract base branch name from gh pr list JSON output, falling back to main when absent."""
97
107
  mock_run.side_effect = [
108
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
98
109
  MagicMock(returncode=0, stdout='[{"baseRefName": "develop", "number": 123}]', stderr=""),
99
110
  MagicMock(returncode=0, stdout="2", stderr=""),
100
111
  ]
@@ -110,10 +121,30 @@ class TestSingleCommitWhenPrExists:
110
121
  timeout=30,
111
122
  )
112
123
 
124
+ mock_run.reset_mock()
125
+ mock_run.side_effect = [
126
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
127
+ MagicMock(returncode=0, stdout='[{"number": 123}]', stderr=""),
128
+ MagicMock(returncode=0, stdout="2", stderr=""),
129
+ ]
130
+
131
+ fallback_violations = check_single_commit_when_pr_exists()
132
+
133
+ assert len(fallback_violations) == 1
134
+ assert "main" in fallback_violations[0].message
135
+ mock_run.assert_any_call(
136
+ ["git", "rev-list", "--count", "main..HEAD"],
137
+ capture_output=True,
138
+ text=True,
139
+ check=True,
140
+ timeout=30,
141
+ )
142
+
113
143
  @patch("git_checks.subprocess.run")
114
144
  def test_non_numeric_commit_count_returns_empty(self, mock_run: MagicMock) -> None:
115
145
  """When git rev-list returns non-numeric output, should return empty."""
116
146
  mock_run.side_effect = [
147
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
117
148
  MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
118
149
  MagicMock(returncode=0, stdout="not a number\n", stderr=""),
119
150
  ]
@@ -125,7 +156,10 @@ class TestSingleCommitWhenPrExists:
125
156
  @patch("git_checks.subprocess.run")
126
157
  def test_gh_timeout_returns_empty(self, mock_run: MagicMock) -> None:
127
158
  """When gh CLI times out, should return empty (warning, not failure)."""
128
- mock_run.side_effect = subprocess.TimeoutExpired(cmd=["gh", "pr", "list"], timeout=30)
159
+ mock_run.side_effect = [
160
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
161
+ subprocess.TimeoutExpired(cmd=["gh", "pr", "list"], timeout=30),
162
+ ]
129
163
 
130
164
  violations = check_single_commit_when_pr_exists()
131
165
 
@@ -135,6 +169,7 @@ class TestSingleCommitWhenPrExists:
135
169
  def test_git_timeout_returns_empty(self, mock_run: MagicMock) -> None:
136
170
  """When git times out, should return empty (warning, not failure)."""
137
171
  mock_run.side_effect = [
172
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
138
173
  MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
139
174
  subprocess.TimeoutExpired(cmd=["git", "rev-list"], timeout=30),
140
175
  ]
@@ -143,6 +178,37 @@ class TestSingleCommitWhenPrExists:
143
178
 
144
179
  assert violations == []
145
180
 
181
+ @patch("git_checks.subprocess.run")
182
+ def test_passes_resolved_branch_name_to_gh(self, mock_run: MagicMock) -> None:
183
+ """gh pr list must receive the resolved branch name, never the literal 'HEAD'."""
184
+ mock_run.side_effect = [
185
+ MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
186
+ MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
187
+ MagicMock(returncode=0, stdout="1", stderr=""),
188
+ ]
189
+
190
+ check_single_commit_when_pr_exists()
191
+
192
+ mock_run.assert_any_call(
193
+ ["gh", "pr", "list", "--head", "feature/my-branch", "--json", "baseRefName,number"],
194
+ capture_output=True,
195
+ text=True,
196
+ check=True,
197
+ timeout=30,
198
+ )
199
+
200
+ @patch("git_checks.subprocess.run")
201
+ def test_unresolved_branch_returns_empty(self, mock_run: MagicMock) -> None:
202
+ """When current branch cannot be resolved, should return empty."""
203
+ mock_run.side_effect = [
204
+ MagicMock(returncode=0, stdout="\n", stderr=""),
205
+ ]
206
+
207
+ violations = check_single_commit_when_pr_exists()
208
+
209
+ assert violations == []
210
+ mock_run.assert_called_once()
211
+
146
212
 
147
213
  class TestDraftPrState:
148
214
  """Test that PR is in draft state when pushing review fixes."""
@@ -208,8 +274,8 @@ class TestDraftPrState:
208
274
  class TestMain:
209
275
  """Test main function integration."""
210
276
 
211
- @patch("git_checks.check_single_commit_when_pr_exists")
212
- @patch("git_checks.check_draft_pr_state")
277
+ @patch("validators.git_checks.check_single_commit_when_pr_exists")
278
+ @patch("validators.git_checks.check_draft_pr_state")
213
279
  def test_main_no_violations_exits_zero(
214
280
  self,
215
281
  mock_draft: MagicMock,
@@ -227,8 +293,8 @@ class TestMain:
227
293
  captured = capsys.readouterr()
228
294
  assert captured.out == ""
229
295
 
230
- @patch("git_checks.check_single_commit_when_pr_exists")
231
- @patch("git_checks.check_draft_pr_state")
296
+ @patch("validators.git_checks.check_single_commit_when_pr_exists")
297
+ @patch("validators.git_checks.check_draft_pr_state")
232
298
  def test_main_with_violations_exits_one(
233
299
  self,
234
300
  mock_draft: MagicMock,
@@ -248,8 +314,8 @@ class TestMain:
248
314
  captured = capsys.readouterr()
249
315
  assert "Branch has 3 commits ahead" in captured.out
250
316
 
251
- @patch("git_checks.check_single_commit_when_pr_exists")
252
- @patch("git_checks.check_draft_pr_state")
317
+ @patch("validators.git_checks.check_single_commit_when_pr_exists")
318
+ @patch("validators.git_checks.check_draft_pr_state")
253
319
  def test_main_prints_violations_without_file_line(
254
320
  self,
255
321
  mock_draft: MagicMock,
@@ -270,8 +336,8 @@ class TestMain:
270
336
  assert captured.out == "PR must be in draft state\n"
271
337
  assert ":0:" not in captured.out
272
338
 
273
- @patch("git_checks.check_single_commit_when_pr_exists")
274
- @patch("git_checks.check_draft_pr_state")
339
+ @patch("validators.git_checks.check_single_commit_when_pr_exists")
340
+ @patch("validators.git_checks.check_draft_pr_state")
275
341
  def test_main_prints_all_violations(
276
342
  self,
277
343
  mock_draft: MagicMock,
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  import pytest
7
7
 
8
- from health_check import (
8
+ from .health_check import (
9
9
  ValidatorHealth,
10
10
  check_validator_exists,
11
11
  check_all_validators,
@@ -6,11 +6,11 @@ from pathlib import Path
6
6
 
7
7
  import pytest
8
8
 
9
- from magic_value_checks import (
9
+ from .magic_value_checks import (
10
10
  check_magic_values,
11
11
  validate_file,
12
12
  )
13
- from validator_base import Violation
13
+ from .validator_base import Violation
14
14
 
15
15
 
16
16
  MAGIC_NUMBER_SOURCE = "x = 42\n"
@@ -3,7 +3,7 @@
3
3
  from pathlib import Path
4
4
  from unittest.mock import patch
5
5
 
6
- from mypy_integration import MypyResult, check_mypy_available, run_mypy_check
6
+ from .mypy_integration import MypyResult, check_mypy_available, run_mypy_check
7
7
 
8
8
 
9
9
  def test_mypy_result_dataclass() -> None:
@@ -1,8 +1,10 @@
1
1
  """Tests for output formatting."""
2
2
 
3
+ import os
4
+
3
5
  import pytest
4
6
 
5
- from output_formatter import (
7
+ from .output_formatter import (
6
8
  OutputFormatter,
7
9
  OutputMode,
8
10
  format_violation_with_context,
@@ -4,11 +4,11 @@ from pathlib import Path
4
4
 
5
5
  import pytest
6
6
 
7
- from pr_reference_checks import (
7
+ from .pr_reference_checks import (
8
8
  check_pr_references,
9
9
  validate_file,
10
10
  )
11
- from validator_base import Violation
11
+ from .validator_base import Violation
12
12
 
13
13
 
14
14
  GOOD_NO_REFERENCES = '''
@@ -4,12 +4,12 @@ import ast
4
4
 
5
5
  import pytest
6
6
 
7
- from python_antipattern_checks import (
7
+ from .python_antipattern_checks import (
8
8
  check_mutable_default_args,
9
9
  check_bare_except,
10
10
  check_print_in_production,
11
11
  )
12
- from validator_base import Violation
12
+ from .validator_base import Violation
13
13
 
14
14
 
15
15
  GOOD_NONE_DEFAULT = '''
@@ -7,12 +7,13 @@ from typing import List
7
7
 
8
8
  import pytest
9
9
 
10
- from python_style_checks import (
10
+ from .python_style_checks import (
11
11
  Violation,
12
12
  check_imports_at_top,
13
13
  check_no_empty_line_after_decorators,
14
14
  check_single_empty_line_between_functions,
15
15
  check_view_function_naming,
16
+ fix_file,
16
17
  validate_file,
17
18
  )
18
19
 
@@ -383,7 +384,6 @@ def foo():
383
384
  temp_path = Path(temp_file.name)
384
385
 
385
386
  try:
386
- from python_style_checks import fix_file
387
387
  fixed = fix_file(temp_path)
388
388
  assert fixed is True
389
389
  result = temp_path.read_text()
@@ -411,7 +411,6 @@ def bar():
411
411
  temp_path = Path(temp_file.name)
412
412
 
413
413
  try:
414
- from python_style_checks import fix_file
415
414
  fixed = fix_file(temp_path)
416
415
  assert fixed is True
417
416
  result = temp_path.read_text()
@@ -432,7 +431,6 @@ def bar():
432
431
  temp_path = Path(temp_file.name)
433
432
 
434
433
  try:
435
- from python_style_checks import fix_file
436
434
  fixed = fix_file(temp_path)
437
435
  assert fixed is False
438
436
  finally:
@@ -2,7 +2,7 @@
2
2
 
3
3
  import pytest
4
4
  from pathlib import Path
5
- from react_checks import check_no_class_components, Violation
5
+ from .react_checks import check_no_class_components, Violation
6
6
 
7
7
 
8
8
  def test_class_component_extends_component_should_fail(tmp_path: Path) -> None:
@@ -3,7 +3,7 @@
3
3
  from pathlib import Path
4
4
  from unittest.mock import patch
5
5
 
6
- from ruff_integration import RuffResult, check_ruff_available, run_ruff_check
6
+ from .ruff_integration import RuffResult, check_ruff_available, run_ruff_check
7
7
 
8
8
 
9
9
  def test_ruff_result_dataclass() -> None:
@@ -6,15 +6,28 @@ from unittest.mock import MagicMock, patch
6
6
 
7
7
  import pytest
8
8
 
9
+ from .run_all_validators import (
10
+ ValidatorResult,
11
+ add_timing,
12
+ build_json_output,
13
+ create_timing_metrics,
14
+ format_timing_report,
15
+ main,
16
+ print_header,
17
+ run_git_checks,
18
+ run_python_style_checks,
19
+ run_with_fallback,
20
+ )
21
+
9
22
 
10
23
  class TestFixFlag:
11
24
  """Test --fix flag functionality."""
12
25
 
13
26
  def test_fix_flag_is_accepted(self) -> None:
14
27
  """Verify --fix flag is recognized without error."""
15
- with patch("run_all_validators.get_changed_files") as mock_get_files, \
16
- patch("run_all_validators.run_file_structure_checks") as mock_file, \
17
- patch("run_all_validators.run_git_checks") as mock_git:
28
+ with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
29
+ patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
30
+ patch("validators.run_all_validators.run_git_checks") as mock_git:
18
31
 
19
32
  mock_get_files.return_value = []
20
33
 
@@ -27,8 +40,6 @@ class TestFixFlag:
27
40
  mock_file.return_value = mock_result
28
41
  mock_git.return_value = mock_result
29
42
 
30
- from run_all_validators import main
31
-
32
43
  original_argv = sys.argv
33
44
  try:
34
45
  sys.argv = ["run_all_validators.py", "--fix"]
@@ -39,14 +50,14 @@ class TestFixFlag:
39
50
 
40
51
  def test_fix_flag_calls_fix_python_style(self) -> None:
41
52
  """Verify --fix flag triggers fix_python_style when files exist."""
42
- with patch("run_all_validators.get_changed_files") as mock_get_files, \
43
- patch("run_all_validators.fix_python_style") as mock_fix, \
44
- patch("run_all_validators.run_python_style_checks") as mock_style, \
45
- patch("run_all_validators.run_test_safety_checks") as mock_test, \
46
- patch("run_all_validators.run_react_checks") as mock_react, \
47
- patch("run_all_validators.run_comment_checks") as mock_comment, \
48
- patch("run_all_validators.run_file_structure_checks") as mock_file, \
49
- patch("run_all_validators.run_git_checks") as mock_git:
53
+ with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
54
+ patch("validators.run_all_validators.fix_python_style") as mock_fix, \
55
+ patch("validators.run_all_validators.run_python_style_checks") as mock_style, \
56
+ patch("validators.run_all_validators.run_test_safety_checks") as mock_test, \
57
+ patch("validators.run_all_validators.run_react_checks") as mock_react, \
58
+ patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
59
+ patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
60
+ patch("validators.run_all_validators.run_git_checks") as mock_git:
50
61
 
51
62
  mock_get_files.return_value = [Path("test.py")]
52
63
  mock_fix.return_value = ["test.py"]
@@ -64,8 +75,6 @@ class TestFixFlag:
64
75
  mock_file.return_value = mock_result
65
76
  mock_git.return_value = mock_result
66
77
 
67
- from run_all_validators import main
68
-
69
78
  original_argv = sys.argv
70
79
  try:
71
80
  sys.argv = ["run_all_validators.py", "--fix"]
@@ -77,14 +86,14 @@ class TestFixFlag:
77
86
 
78
87
  def test_no_fix_flag_skips_fixes(self) -> None:
79
88
  """Verify fixes are skipped when --fix flag is not provided."""
80
- with patch("run_all_validators.get_changed_files") as mock_get_files, \
81
- patch("run_all_validators.fix_python_style") as mock_fix, \
82
- patch("run_all_validators.run_python_style_checks") as mock_style, \
83
- patch("run_all_validators.run_test_safety_checks") as mock_test, \
84
- patch("run_all_validators.run_react_checks") as mock_react, \
85
- patch("run_all_validators.run_comment_checks") as mock_comment, \
86
- patch("run_all_validators.run_file_structure_checks") as mock_file, \
87
- patch("run_all_validators.run_git_checks") as mock_git:
89
+ with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
90
+ patch("validators.run_all_validators.fix_python_style") as mock_fix, \
91
+ patch("validators.run_all_validators.run_python_style_checks") as mock_style, \
92
+ patch("validators.run_all_validators.run_test_safety_checks") as mock_test, \
93
+ patch("validators.run_all_validators.run_react_checks") as mock_react, \
94
+ patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
95
+ patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
96
+ patch("validators.run_all_validators.run_git_checks") as mock_git:
88
97
 
89
98
  mock_get_files.return_value = [Path("test.py")]
90
99
 
@@ -101,8 +110,6 @@ class TestFixFlag:
101
110
  mock_file.return_value = mock_result
102
111
  mock_git.return_value = mock_result
103
112
 
104
- from run_all_validators import main
105
-
106
113
  original_argv = sys.argv
107
114
  try:
108
115
  sys.argv = ["run_all_validators.py"]
@@ -115,8 +122,6 @@ class TestFixFlag:
115
122
 
116
123
  class TestGracefulDegradation:
117
124
  def test_missing_validator_returns_skipped_result(self) -> None:
118
- from run_all_validators import ValidatorResult, run_with_fallback
119
-
120
125
  def failing_validator() -> ValidatorResult:
121
126
  raise FileNotFoundError("validator.py not found")
122
127
 
@@ -131,8 +136,6 @@ class TestGracefulDegradation:
131
136
  assert result.passed is False
132
137
 
133
138
  def test_validator_exception_returns_skipped_result(self) -> None:
134
- from run_all_validators import ValidatorResult, run_with_fallback
135
-
136
139
  def crashing_validator() -> ValidatorResult:
137
140
  raise RuntimeError("Unexpected crash")
138
141
 
@@ -146,8 +149,6 @@ class TestGracefulDegradation:
146
149
  assert "skipped" in result.output.lower()
147
150
 
148
151
  def test_successful_validator_returns_normal_result(self) -> None:
149
- from run_all_validators import ValidatorResult, run_with_fallback
150
-
151
152
  def working_validator() -> ValidatorResult:
152
153
  return ValidatorResult(
153
154
  name="Working",
@@ -166,25 +167,62 @@ class TestGracefulDegradation:
166
167
  assert result.passed is True
167
168
 
168
169
 
170
+ class TestStderrSurfacing:
171
+ """Verify that validator stderr is surfaced when stdout is empty."""
172
+
173
+ def test_python_style_check_surfaces_stderr_when_stdout_empty(self) -> None:
174
+ """When a validator crashes with no stdout, stderr must appear in output."""
175
+ with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
176
+ crashed_result = MagicMock()
177
+ crashed_result.returncode = 1
178
+ crashed_result.stdout = ""
179
+ crashed_result.stderr = "ImportError: No module named validators.python_style_checks"
180
+ mock_invoke.return_value = crashed_result
181
+
182
+ validator_result = run_python_style_checks([Path("foo.py")])
183
+
184
+ assert "ImportError" in validator_result.output
185
+
186
+ def test_git_check_surfaces_stderr_when_stdout_empty(self) -> None:
187
+ """When git validator crashes with no stdout, stderr must appear in output."""
188
+ with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
189
+ crashed_result = MagicMock()
190
+ crashed_result.returncode = 1
191
+ crashed_result.stdout = ""
192
+ crashed_result.stderr = "SyntaxError: invalid syntax in git_checks.py"
193
+ mock_invoke.return_value = crashed_result
194
+
195
+ validator_result = run_git_checks()
196
+
197
+ assert "SyntaxError" in validator_result.output
198
+
199
+ def test_output_falls_back_to_all_checks_passed_when_both_empty(self) -> None:
200
+ """When both stdout and stderr are empty and returncode is 0, use fallback."""
201
+ with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
202
+ clean_result = MagicMock()
203
+ clean_result.returncode = 0
204
+ clean_result.stdout = ""
205
+ clean_result.stderr = ""
206
+ mock_invoke.return_value = clean_result
207
+
208
+ validator_result = run_git_checks()
209
+
210
+ assert validator_result.output == "All checks passed"
211
+
212
+
169
213
  class TestTimingMetrics:
170
214
  def test_create_timing_metrics_empty(self) -> None:
171
- from run_all_validators import create_timing_metrics
172
-
173
215
  metrics = create_timing_metrics({})
174
216
  assert metrics.total_seconds == 0.0
175
217
  assert metrics.validator_times == {}
176
218
 
177
219
  def test_create_timing_metrics_with_data(self) -> None:
178
- from run_all_validators import create_timing_metrics
179
-
180
220
  timings = {"Validator A": 1.5, "Validator B": 2.0}
181
221
  metrics = create_timing_metrics(timings)
182
222
  assert metrics.total_seconds == 3.5
183
223
  assert metrics.validator_times == timings
184
224
 
185
225
  def test_add_timing_returns_new_instance(self) -> None:
186
- from run_all_validators import add_timing, create_timing_metrics
187
-
188
226
  metrics1 = create_timing_metrics({})
189
227
  metrics2 = add_timing(metrics1, "Test", 1.5)
190
228
 
@@ -194,8 +232,6 @@ class TestTimingMetrics:
194
232
  assert metrics2.validator_times["Test"] == 1.5
195
233
 
196
234
  def test_format_report_includes_all_timings(self) -> None:
197
- from run_all_validators import create_timing_metrics, format_timing_report
198
-
199
235
  metrics = create_timing_metrics({"Fast": 0.1, "Slow": 2.5})
200
236
  report = format_timing_report(metrics)
201
237
 
@@ -206,8 +242,6 @@ class TestTimingMetrics:
206
242
 
207
243
  class TestVersionHeader:
208
244
  def test_print_header_includes_version(self, capsys) -> None:
209
- from run_all_validators import print_header
210
-
211
245
  print_header()
212
246
  captured = capsys.readouterr()
213
247
 
@@ -215,8 +249,6 @@ class TestVersionHeader:
215
249
  assert "(v" in captured.out
216
250
 
217
251
  def test_build_json_output_includes_version(self) -> None:
218
- from run_all_validators import build_json_output, create_timing_metrics
219
-
220
252
  json_output = build_json_output(
221
253
  results=[],
222
254
  metrics=create_timing_metrics({}),
@@ -4,45 +4,22 @@ import subprocess
4
4
  import sys
5
5
  from pathlib import Path
6
6
 
7
- import pytest
8
-
9
-
10
7
  VALIDATORS_DIR = Path(__file__).parent
8
+ HOOKS_DIR = VALIDATORS_DIR.parent
9
+ PACKAGE_MODULE = f"{VALIDATORS_DIR.name}.run_all_validators"
11
10
 
12
11
 
13
- class TestNewValidatorsIntegration:
14
- def test_abbreviation_checks_called(self) -> None:
15
- """Verify abbreviation_checks is invoked by run_all_validators."""
16
- result = subprocess.run(
17
- [sys.executable, str(VALIDATORS_DIR / "run_all_validators.py"), "--help"],
18
- capture_output=True,
19
- text=True,
20
- )
21
- assert "Abbreviations" in result.stdout or result.returncode == 0
12
+ def run_validators_help() -> subprocess.CompletedProcess[str]:
13
+ return subprocess.run(
14
+ [sys.executable, "-m", PACKAGE_MODULE, "--help"],
15
+ capture_output=True,
16
+ text=True,
17
+ cwd=str(HOOKS_DIR),
18
+ )
22
19
 
23
- def test_pr_reference_checks_called(self) -> None:
24
- """Verify pr_reference_checks is invoked by run_all_validators."""
25
- result = subprocess.run(
26
- [sys.executable, str(VALIDATORS_DIR / "run_all_validators.py"), "--help"],
27
- capture_output=True,
28
- text=True,
29
- )
30
- assert "PR References" in result.stdout or result.returncode == 0
31
20
 
32
- def test_magic_value_checks_called(self) -> None:
33
- """Verify magic_value_checks is invoked by run_all_validators."""
34
- result = subprocess.run(
35
- [sys.executable, str(VALIDATORS_DIR / "run_all_validators.py"), "--help"],
36
- capture_output=True,
37
- text=True,
38
- )
39
- assert "Magic Values" in result.stdout or result.returncode == 0
40
-
41
- def test_useless_test_checks_called(self) -> None:
42
- """Verify useless_test_checks is invoked by run_all_validators."""
43
- result = subprocess.run(
44
- [sys.executable, str(VALIDATORS_DIR / "run_all_validators.py"), "--help"],
45
- capture_output=True,
46
- text=True,
47
- )
48
- assert "Useless Tests" in result.stdout or result.returncode == 0
21
+ class TestNewValidatorsIntegration:
22
+ def test_help_exits_cleanly(self) -> None:
23
+ """Verify run_all_validators --help exits with code 0."""
24
+ result = run_validators_help()
25
+ assert result.returncode == 0, result.stderr
@@ -4,12 +4,12 @@ import ast
4
4
 
5
5
  import pytest
6
6
 
7
- from security_checks import (
7
+ from .security_checks import (
8
8
  check_hardcoded_secrets,
9
9
  check_sql_injection,
10
10
  check_xss_risk,
11
11
  )
12
- from validator_base import Violation
12
+ from .validator_base import Violation
13
13
 
14
14
 
15
15
  GOOD_NO_SECRETS = '''
@@ -7,7 +7,7 @@ from typing import List
7
7
 
8
8
  import pytest
9
9
 
10
- from test_safety_checks import (
10
+ from .test_safety_checks import (
11
11
  Violation,
12
12
  check_no_skip_decorators,
13
13
  check_debug_guard_in_dev_scripts,
@@ -2,8 +2,8 @@
2
2
 
3
3
  import pytest
4
4
 
5
- from todo_checks import check_untracked_todos
6
- from validator_base import Violation
5
+ from .todo_checks import check_untracked_todos
6
+ from .validator_base import Violation
7
7
 
8
8
 
9
9
  GOOD_TODO_WITH_ISSUE = '''