claude-dev-env 1.25.2 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -0
- package/agents/clean-coder.md +1 -1
- package/docs/CODE_RULES.md +3 -1
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +54 -0
- package/hooks/blocking/{code-rules-enforcer.py → code_rules_enforcer.py} +154 -5
- package/hooks/blocking/test_code_rules_enforcer.py +61 -0
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +183 -0
- package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/test_pr_description_enforcer.py +8 -8
- package/hooks/blocking/test_tdd_enforcer.py +1 -1
- package/hooks/github-action/pre-push-review.yml +27 -0
- package/hooks/hooks.json +28 -28
- package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +26 -12
- package/hooks/lifecycle/test_config_change_guard.py +3 -3
- package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
- package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
- package/hooks/notification/notification_utils.py +56 -0
- package/hooks/notification/subagent_complete_notify.py +381 -0
- package/hooks/notification/test_attention_needed_notify.py +47 -0
- package/hooks/notification/test_claude_notification_handler.py +54 -0
- package/hooks/notification/test_notification_utils.py +45 -0
- package/hooks/notification/test_subagent_complete_notify.py +79 -0
- package/hooks/validators/README.md +5 -1
- package/hooks/validators/abbreviation_checks.py +1 -1
- package/hooks/validators/code_quality_checks.py +1 -1
- package/hooks/validators/config.py +5 -0
- package/hooks/validators/conftest.py +10 -0
- package/hooks/validators/exempt_paths.py +1 -1
- package/hooks/validators/git_checks.py +80 -0
- package/hooks/validators/magic_value_checks.py +2 -2
- package/hooks/validators/pr_reference_checks.py +1 -1
- package/hooks/validators/python_antipattern_checks.py +1 -1
- package/hooks/validators/run_all_validators.py +53 -105
- package/hooks/validators/security_checks.py +1 -1
- package/hooks/validators/test_abbreviation_checks.py +2 -2
- package/hooks/validators/test_code_quality_checks.py +2 -2
- package/hooks/validators/test_file_structure_checks.py +1 -1
- package/hooks/validators/test_git_checks.py +79 -13
- package/hooks/validators/test_health_check.py +1 -1
- package/hooks/validators/test_magic_value_checks.py +2 -2
- package/hooks/validators/test_mypy_integration.py +1 -1
- package/hooks/validators/test_output_formatter.py +3 -1
- package/hooks/validators/test_pr_reference_checks.py +2 -2
- package/hooks/validators/test_python_antipattern_checks.py +2 -2
- package/hooks/validators/test_python_style_checks.py +2 -4
- package/hooks/validators/test_react_checks.py +1 -1
- package/hooks/validators/test_ruff_integration.py +1 -1
- package/hooks/validators/test_run_all_validators.py +75 -43
- package/hooks/validators/test_run_all_validators_integration.py +14 -37
- package/hooks/validators/test_security_checks.py +2 -2
- package/hooks/validators/test_test_safety_checks.py +1 -1
- package/hooks/validators/test_todo_checks.py +2 -2
- package/hooks/validators/test_type_safety_checks.py +2 -2
- package/hooks/validators/test_useless_test_checks.py +2 -2
- package/hooks/validators/test_validator_base.py +1 -1
- package/hooks/validators/test_verify_paths.py +2 -4
- package/hooks/validators/todo_checks.py +1 -1
- package/hooks/validators/type_safety_checks.py +1 -1
- package/hooks/validators/useless_test_checks.py +1 -1
- package/package.json +1 -1
- package/rules/file-global-constants.md +71 -0
- package/rules/gh-body-file.md +1 -1
- package/rules/prompt-workflow-context-controls.md +48 -0
- package/scripts/sync_to_cursor/rules.py +2 -2
- package/scripts/tests/test_sync_to_cursor.py +2 -2
- package/skills/bugteam/CONSTRAINTS.md +37 -0
- package/skills/bugteam/EXAMPLES.md +64 -0
- package/skills/bugteam/PROMPTS.md +175 -0
- package/skills/bugteam/SKILL.md +204 -295
- package/skills/bugteam/SKILL_EVALS.md +346 -0
- package/skills/bugteam/scripts/README.md +37 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
- package/skills/rule-audit/SKILL.md +4 -4
- /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
- /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
- /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
- /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.py} +0 -0
- /package/hooks/blocking/{destructive-command-blocker.py → destructive_command_blocker.py} +0 -0
- /package/hooks/blocking/{gh-body-arg-blocker.py → gh_body_arg_blocker.py} +0 -0
- /package/hooks/blocking/{hedging-language-blocker.py → hedging_language_blocker.py} +0 -0
- /package/hooks/blocking/{pr-description-enforcer.py → pr_description_enforcer.py} +0 -0
- /package/hooks/blocking/{sensitive-file-protector.py → sensitive_file_protector.py} +0 -0
- /package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +0 -0
- /package/hooks/blocking/{test-preflight-check.py → test_preflight_check.py} +0 -0
- /package/hooks/blocking/{write-existing-file-blocker.py → write_existing_file_blocker.py} +0 -0
- /package/hooks/git-hooks/{post-commit.py → post_commit.py} +0 -0
- /package/hooks/lifecycle/{session-end-cleanup.py → session_end_cleanup.py} +0 -0
- /package/hooks/{rewrite-plugin-paths.py → rewrite_plugin_paths.py} +0 -0
- /package/hooks/session/{plugin-data-dir-cleanup.py → plugin_data_dir_cleanup.py} +0 -0
- /package/hooks/validation/{hook-format-validator.py → hook_format_validator.py} +0 -0
- /package/hooks/workflow/{auto-formatter.py → auto_formatter.py} +0 -0
- /package/hooks/workflow/{investigation-tracker-reset.py → investigation_tracker_reset.py} +0 -0
- /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.
|
|
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 =
|
|
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 =
|
|
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,
|
|
@@ -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:
|
|
@@ -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:
|
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 = '''
|