claude-dev-env 1.0.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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/agents/agent-writer.md +157 -0
- package/agents/clasp-deployment-orchestrator.md +609 -0
- package/agents/clean-coder.md +295 -0
- package/agents/code-quality-agent.md +40 -0
- package/agents/code-standards-agent.md +93 -0
- package/agents/config-centralizer.md +686 -0
- package/agents/config-extraction-agent.md +225 -0
- package/agents/doc-orchestrator.md +47 -0
- package/agents/docs-agent.md +112 -0
- package/agents/docx-agent.md +211 -0
- package/agents/git-commit-crafter.md +100 -0
- package/agents/magic-value-eliminator-agent.md +72 -0
- package/agents/mandatory-agent-workflow-agent.md +88 -0
- package/agents/parallel-workflow-coordinator.md +779 -0
- package/agents/pdf-agent.md +302 -0
- package/agents/plan-executor.md +226 -0
- package/agents/pr-description-writer.md +87 -0
- package/agents/project-context-loader.md +238 -0
- package/agents/project-docs-analyzer.md +54 -0
- package/agents/project-structure-organizer-agent.md +72 -0
- package/agents/readability-review-agent.md +76 -0
- package/agents/refactoring-specialist.md +69 -0
- package/agents/right-sized-engineer.md +129 -0
- package/agents/session-continuity-manager.md +53 -0
- package/agents/skill-to-agent-converter.md +371 -0
- package/agents/skill-writer-agent.md +470 -0
- package/agents/stub-detector-agent.md +140 -0
- package/agents/tdd-test-writer.md +62 -0
- package/agents/test-data-builder.md +68 -0
- package/agents/tooling-builder.md +78 -0
- package/agents/user-docs-writer.md +67 -0
- package/agents/validation-expert.md +71 -0
- package/agents/workflow-visual-documenter.md +82 -0
- package/agents/xlsx-agent.md +169 -0
- package/bin/install.mjs +256 -0
- package/commands/commit.md +28 -0
- package/commands/docupdate.md +322 -0
- package/commands/implement.md +102 -0
- package/commands/initialize.md +91 -0
- package/commands/plan.md +63 -0
- package/commands/pr-comments.md +47 -0
- package/commands/readability-review.md +20 -0
- package/commands/review-plan.md +7 -0
- package/commands/right-size.md +15 -0
- package/commands/stubcheck.md +89 -0
- package/commands/sum.md +30 -0
- package/docs/CODE_RULES.md +186 -0
- package/docs/DJANGO_PATTERNS.md +80 -0
- package/docs/REACT_PATTERNS.md +185 -0
- package/docs/TEST_QUALITY.md +104 -0
- package/hooks/advisory/migration-safety-advisor.py +49 -0
- package/hooks/advisory/refactor-guard.py +205 -0
- package/hooks/blocking/block-main-commit.py +168 -0
- package/hooks/blocking/code-rules-enforcer.py +549 -0
- package/hooks/blocking/destructive-command-blocker.py +107 -0
- package/hooks/blocking/docker-settings-guard.py +44 -0
- package/hooks/blocking/hedging-language-blocker.py +130 -0
- package/hooks/blocking/parallel-task-blocker.py +69 -0
- package/hooks/blocking/pr-description-enforcer.py +87 -0
- package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
- package/hooks/blocking/sensitive-file-protector.py +70 -0
- package/hooks/blocking/tdd-enforcer.py +62 -0
- package/hooks/blocking/test-preflight-check.py +343 -0
- package/hooks/blocking/write-existing-file-blocker.py +63 -0
- package/hooks/git-hooks/post-commit.py +103 -0
- package/hooks/github-action/test_workflow.py +33 -0
- package/hooks/hooks.json +246 -0
- package/hooks/lifecycle/config-change-guard.py +84 -0
- package/hooks/lifecycle/session-end-cleanup.py +59 -0
- package/hooks/notification/attention-needed-notify.py +63 -0
- package/hooks/notification/claude-notification-handler.py +59 -0
- package/hooks/notification/notification_utils.py +206 -0
- package/hooks/rewrite-plugin-paths.py +116 -0
- package/hooks/session/bulk-edit-reminder.py +30 -0
- package/hooks/session/code-rules-reminder.py +97 -0
- package/hooks/session/compact-context-reinject.py +39 -0
- package/hooks/session/hook-structure-context.py +140 -0
- package/hooks/session/plugin-data-dir-cleanup.py +39 -0
- package/hooks/validation/code-style-validator.py +145 -0
- package/hooks/validation/e2e-test-validator.py +142 -0
- package/hooks/validation/hook-format-validator.py +66 -0
- package/hooks/validation/mypy_validator.py +180 -0
- package/hooks/validators/README.md +125 -0
- package/hooks/validators/VALIDATION_REPORT.md +287 -0
- package/hooks/validators/__init__.py +19 -0
- package/hooks/validators/abbreviation_checks.py +82 -0
- package/hooks/validators/code_quality_checks.py +133 -0
- package/hooks/validators/comment_checks.py +188 -0
- package/hooks/validators/file_structure_checks.py +182 -0
- package/hooks/validators/git_checks.py +107 -0
- package/hooks/validators/health_check.py +214 -0
- package/hooks/validators/magic_value_checks.py +81 -0
- package/hooks/validators/mypy_integration.py +52 -0
- package/hooks/validators/output_formatter.py +266 -0
- package/hooks/validators/pr_reference_checks.py +72 -0
- package/hooks/validators/python_antipattern_checks.py +110 -0
- package/hooks/validators/python_style_checks.py +364 -0
- package/hooks/validators/react_checks.py +90 -0
- package/hooks/validators/ruff_integration.py +80 -0
- package/hooks/validators/run_all_validators.py +772 -0
- package/hooks/validators/security_checks.py +135 -0
- package/hooks/validators/test_abbreviation_checks.py +76 -0
- package/hooks/validators/test_bad.tsx +7 -0
- package/hooks/validators/test_code_quality_checks.py +129 -0
- package/hooks/validators/test_file_structure_checks.py +307 -0
- package/hooks/validators/test_files/01_basic_component.tsx +10 -0
- package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
- package/hooks/validators/test_files/03_pure_component.tsx +10 -0
- package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
- package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
- package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
- package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
- package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
- package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
- package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
- package/hooks/validators/test_files/11_ts_file.ts +10 -0
- package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
- package/hooks/validators/test_files/13_functional_component.tsx +8 -0
- package/hooks/validators/test_files/14_indented_class.tsx +13 -0
- package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
- package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
- package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
- package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
- package/hooks/validators/test_files/async_views.py +23 -0
- package/hooks/validators/test_files/async_with_imports.py +14 -0
- package/hooks/validators/test_files/bad_inline_imports.py +37 -0
- package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
- package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
- package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
- package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
- package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
- package/hooks/validators/test_files/not_management_command.py +10 -0
- package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
- package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
- package/hooks/validators/test_files/test_async_functions.py +45 -0
- package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
- package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
- package/hooks/validators/test_git_checks.py +295 -0
- package/hooks/validators/test_good.tsx +5 -0
- package/hooks/validators/test_health_check.py +57 -0
- package/hooks/validators/test_magic_value_checks.py +63 -0
- package/hooks/validators/test_mypy_integration.py +27 -0
- package/hooks/validators/test_output_formatter.py +150 -0
- package/hooks/validators/test_pr_reference_checks.py +41 -0
- package/hooks/validators/test_python_antipattern_checks.py +113 -0
- package/hooks/validators/test_python_style_checks.py +439 -0
- package/hooks/validators/test_react_checks.py +213 -0
- package/hooks/validators/test_results.txt +25 -0
- package/hooks/validators/test_ruff_integration.py +27 -0
- package/hooks/validators/test_run_all_validators.py +228 -0
- package/hooks/validators/test_run_all_validators_integration.py +48 -0
- package/hooks/validators/test_safety_checks.py +243 -0
- package/hooks/validators/test_security_checks.py +105 -0
- package/hooks/validators/test_test_safety_checks.py +321 -0
- package/hooks/validators/test_todo_checks.py +39 -0
- package/hooks/validators/test_type_safety_checks.py +85 -0
- package/hooks/validators/test_useless_test_checks.py +55 -0
- package/hooks/validators/test_validator_base.py +26 -0
- package/hooks/validators/test_verify_paths.py +34 -0
- package/hooks/validators/todo_checks.py +59 -0
- package/hooks/validators/type_safety_checks.py +101 -0
- package/hooks/validators/useless_test_checks.py +92 -0
- package/hooks/validators/validator_base.py +19 -0
- package/hooks/validators/verify_paths.py +57 -0
- package/hooks/workflow/auto-formatter.py +114 -0
- package/hooks/workflow/investigation-tracker-reset.py +46 -0
- package/package.json +30 -0
- package/rules/agent-spawn-protocol.md +47 -0
- package/rules/cleanup-temp-files.md +27 -0
- package/rules/code-reviews.md +11 -0
- package/rules/code-standards.md +43 -0
- package/rules/conservative-action.md +20 -0
- package/rules/context7.md +12 -0
- package/rules/explore-thoroughly.md +27 -0
- package/rules/git-workflow.md +42 -0
- package/rules/parallel-tools.md +23 -0
- package/rules/research-mode.md +23 -0
- package/rules/right-sized-engineering.md +28 -0
- package/rules/tdd.md +7 -0
- package/rules/testing.md +12 -0
- package/skills/agent-prompt/SKILL.md +102 -0
- package/skills/anthropic-plan/SKILL.md +107 -0
- package/skills/everything-search/SKILL.md +144 -0
- package/skills/ingest/SKILL.md +40 -0
- package/skills/npm-creator/SKILL.md +183 -0
- package/skills/pr-review-responder/EXAMPLES.md +590 -0
- package/skills/pr-review-responder/PRINCIPLES.md +539 -0
- package/skills/pr-review-responder/README.md +209 -0
- package/skills/pr-review-responder/SKILL.md +202 -0
- package/skills/pr-review-responder/TESTING.md +407 -0
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
- package/skills/pr-review-responder/update_skill.py +297 -0
- package/skills/prompt-generator/REFERENCE.md +150 -0
- package/skills/prompt-generator/SKILL.md +154 -0
- package/skills/readability-review/SKILL.md +127 -0
- package/skills/recall/SKILL.md +27 -0
- package/skills/remember/SKILL.md +63 -0
- package/skills/rule-audit/SKILL.md +307 -0
- package/skills/rule-creator/SKILL.md +150 -0
- package/skills/skill-writer/REFERENCE.md +246 -0
- package/skills/skill-writer/SKILL.md +270 -0
- package/skills/tdd-team/SKILL.md +128 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Tests for run_all_validators.py."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestFixFlag:
|
|
11
|
+
"""Test --fix flag functionality."""
|
|
12
|
+
|
|
13
|
+
def test_fix_flag_is_accepted(self) -> None:
|
|
14
|
+
"""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:
|
|
18
|
+
|
|
19
|
+
mock_get_files.return_value = []
|
|
20
|
+
|
|
21
|
+
mock_result = MagicMock()
|
|
22
|
+
mock_result.passed = True
|
|
23
|
+
mock_result.name = "Test"
|
|
24
|
+
mock_result.checks = "test"
|
|
25
|
+
mock_result.output = ""
|
|
26
|
+
|
|
27
|
+
mock_file.return_value = mock_result
|
|
28
|
+
mock_git.return_value = mock_result
|
|
29
|
+
|
|
30
|
+
from run_all_validators import main
|
|
31
|
+
|
|
32
|
+
original_argv = sys.argv
|
|
33
|
+
try:
|
|
34
|
+
sys.argv = ["run_all_validators.py", "--fix"]
|
|
35
|
+
result = main()
|
|
36
|
+
assert result == 0
|
|
37
|
+
finally:
|
|
38
|
+
sys.argv = original_argv
|
|
39
|
+
|
|
40
|
+
def test_fix_flag_calls_fix_python_style(self) -> None:
|
|
41
|
+
"""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:
|
|
50
|
+
|
|
51
|
+
mock_get_files.return_value = [Path("test.py")]
|
|
52
|
+
mock_fix.return_value = ["test.py"]
|
|
53
|
+
|
|
54
|
+
mock_result = MagicMock()
|
|
55
|
+
mock_result.passed = True
|
|
56
|
+
mock_result.name = "Test"
|
|
57
|
+
mock_result.checks = "test"
|
|
58
|
+
mock_result.output = ""
|
|
59
|
+
|
|
60
|
+
mock_style.return_value = mock_result
|
|
61
|
+
mock_test.return_value = mock_result
|
|
62
|
+
mock_react.return_value = mock_result
|
|
63
|
+
mock_comment.return_value = mock_result
|
|
64
|
+
mock_file.return_value = mock_result
|
|
65
|
+
mock_git.return_value = mock_result
|
|
66
|
+
|
|
67
|
+
from run_all_validators import main
|
|
68
|
+
|
|
69
|
+
original_argv = sys.argv
|
|
70
|
+
try:
|
|
71
|
+
sys.argv = ["run_all_validators.py", "--fix"]
|
|
72
|
+
main()
|
|
73
|
+
finally:
|
|
74
|
+
sys.argv = original_argv
|
|
75
|
+
|
|
76
|
+
mock_fix.assert_called_once()
|
|
77
|
+
|
|
78
|
+
def test_no_fix_flag_skips_fixes(self) -> None:
|
|
79
|
+
"""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:
|
|
88
|
+
|
|
89
|
+
mock_get_files.return_value = [Path("test.py")]
|
|
90
|
+
|
|
91
|
+
mock_result = MagicMock()
|
|
92
|
+
mock_result.passed = True
|
|
93
|
+
mock_result.name = "Test"
|
|
94
|
+
mock_result.checks = "test"
|
|
95
|
+
mock_result.output = ""
|
|
96
|
+
|
|
97
|
+
mock_style.return_value = mock_result
|
|
98
|
+
mock_test.return_value = mock_result
|
|
99
|
+
mock_react.return_value = mock_result
|
|
100
|
+
mock_comment.return_value = mock_result
|
|
101
|
+
mock_file.return_value = mock_result
|
|
102
|
+
mock_git.return_value = mock_result
|
|
103
|
+
|
|
104
|
+
from run_all_validators import main
|
|
105
|
+
|
|
106
|
+
original_argv = sys.argv
|
|
107
|
+
try:
|
|
108
|
+
sys.argv = ["run_all_validators.py"]
|
|
109
|
+
main()
|
|
110
|
+
finally:
|
|
111
|
+
sys.argv = original_argv
|
|
112
|
+
|
|
113
|
+
mock_fix.assert_not_called()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestGracefulDegradation:
|
|
117
|
+
def test_missing_validator_returns_skipped_result(self) -> None:
|
|
118
|
+
from run_all_validators import ValidatorResult, run_with_fallback
|
|
119
|
+
|
|
120
|
+
def failing_validator() -> ValidatorResult:
|
|
121
|
+
raise FileNotFoundError("validator.py not found")
|
|
122
|
+
|
|
123
|
+
result = run_with_fallback(
|
|
124
|
+
failing_validator,
|
|
125
|
+
"Missing Validator",
|
|
126
|
+
"99",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
assert result.skipped is True
|
|
130
|
+
assert "skipped" in result.output.lower()
|
|
131
|
+
assert result.passed is False
|
|
132
|
+
|
|
133
|
+
def test_validator_exception_returns_skipped_result(self) -> None:
|
|
134
|
+
from run_all_validators import ValidatorResult, run_with_fallback
|
|
135
|
+
|
|
136
|
+
def crashing_validator() -> ValidatorResult:
|
|
137
|
+
raise RuntimeError("Unexpected crash")
|
|
138
|
+
|
|
139
|
+
result = run_with_fallback(
|
|
140
|
+
crashing_validator,
|
|
141
|
+
"Crashing Validator",
|
|
142
|
+
"99",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
assert result.skipped is True
|
|
146
|
+
assert "skipped" in result.output.lower()
|
|
147
|
+
|
|
148
|
+
def test_successful_validator_returns_normal_result(self) -> None:
|
|
149
|
+
from run_all_validators import ValidatorResult, run_with_fallback
|
|
150
|
+
|
|
151
|
+
def working_validator() -> ValidatorResult:
|
|
152
|
+
return ValidatorResult(
|
|
153
|
+
name="Working",
|
|
154
|
+
checks="1",
|
|
155
|
+
passed=True,
|
|
156
|
+
output="All good",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
result = run_with_fallback(
|
|
160
|
+
working_validator,
|
|
161
|
+
"Working",
|
|
162
|
+
"1",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
assert result.skipped is False
|
|
166
|
+
assert result.passed is True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TestTimingMetrics:
|
|
170
|
+
def test_create_timing_metrics_empty(self) -> None:
|
|
171
|
+
from run_all_validators import create_timing_metrics
|
|
172
|
+
|
|
173
|
+
metrics = create_timing_metrics({})
|
|
174
|
+
assert metrics.total_seconds == 0.0
|
|
175
|
+
assert metrics.validator_times == {}
|
|
176
|
+
|
|
177
|
+
def test_create_timing_metrics_with_data(self) -> None:
|
|
178
|
+
from run_all_validators import create_timing_metrics
|
|
179
|
+
|
|
180
|
+
timings = {"Validator A": 1.5, "Validator B": 2.0}
|
|
181
|
+
metrics = create_timing_metrics(timings)
|
|
182
|
+
assert metrics.total_seconds == 3.5
|
|
183
|
+
assert metrics.validator_times == timings
|
|
184
|
+
|
|
185
|
+
def test_add_timing_returns_new_instance(self) -> None:
|
|
186
|
+
from run_all_validators import add_timing, create_timing_metrics
|
|
187
|
+
|
|
188
|
+
metrics1 = create_timing_metrics({})
|
|
189
|
+
metrics2 = add_timing(metrics1, "Test", 1.5)
|
|
190
|
+
|
|
191
|
+
assert metrics1.total_seconds == 0.0
|
|
192
|
+
assert metrics2.total_seconds == 1.5
|
|
193
|
+
assert "Test" not in metrics1.validator_times
|
|
194
|
+
assert metrics2.validator_times["Test"] == 1.5
|
|
195
|
+
|
|
196
|
+
def test_format_report_includes_all_timings(self) -> None:
|
|
197
|
+
from run_all_validators import create_timing_metrics, format_timing_report
|
|
198
|
+
|
|
199
|
+
metrics = create_timing_metrics({"Fast": 0.1, "Slow": 2.5})
|
|
200
|
+
report = format_timing_report(metrics)
|
|
201
|
+
|
|
202
|
+
assert "Fast" in report
|
|
203
|
+
assert "Slow" in report
|
|
204
|
+
assert "2.6" in report
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestVersionHeader:
|
|
208
|
+
def test_print_header_includes_version(self, capsys) -> None:
|
|
209
|
+
from run_all_validators import print_header
|
|
210
|
+
|
|
211
|
+
print_header()
|
|
212
|
+
captured = capsys.readouterr()
|
|
213
|
+
|
|
214
|
+
assert "PRE-PUSH VALIDATOR RESULTS" in captured.out
|
|
215
|
+
assert "(v" in captured.out
|
|
216
|
+
|
|
217
|
+
def test_build_json_output_includes_version(self) -> None:
|
|
218
|
+
from run_all_validators import build_json_output, create_timing_metrics
|
|
219
|
+
|
|
220
|
+
json_output = build_json_output(
|
|
221
|
+
results=[],
|
|
222
|
+
metrics=create_timing_metrics({}),
|
|
223
|
+
include_timing=False,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert "version" in json_output
|
|
227
|
+
assert "timestamp" in json_output
|
|
228
|
+
assert isinstance(json_output["version"], str)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Integration test for new validators in run_all_validators.py"""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
VALIDATORS_DIR = Path(__file__).parent
|
|
11
|
+
|
|
12
|
+
|
|
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
|
|
22
|
+
|
|
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
|
+
|
|
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
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""AST-based safety validators for test files and development scripts.
|
|
3
|
+
|
|
4
|
+
This module provides checks for:
|
|
5
|
+
1. No skip decorators in test files (tests must fail, not skip)
|
|
6
|
+
2. DEBUG guard in Django management commands (dev tools only for DEBUG mode)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SKIP_DECORATOR_NAMES = frozenset([
|
|
17
|
+
"skip",
|
|
18
|
+
"skipif",
|
|
19
|
+
"skipunless",
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
SKIP_DECORATOR_MESSAGE = (
|
|
23
|
+
"Skip decorator not allowed. Tests should fail if they can't run. "
|
|
24
|
+
"Missing dependencies should make the test fail with a clear error."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
DEBUG_CHECK_MESSAGE = (
|
|
28
|
+
"Management command in management/commands/ must check settings.DEBUG. "
|
|
29
|
+
"Dev tools should only run in DEBUG mode."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Violation:
|
|
35
|
+
"""Represents a code violation found by a validator."""
|
|
36
|
+
|
|
37
|
+
file: str
|
|
38
|
+
line: int
|
|
39
|
+
message: str
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return f"{self.file}:{self.line}: {self.message}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_no_skip_decorators(code: str, filepath: str) -> List[Violation]:
|
|
46
|
+
"""Check that test files don't use skip decorators.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
code: Python source code to check
|
|
50
|
+
filepath: Path to the file being checked (for error reporting)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of violations found
|
|
54
|
+
"""
|
|
55
|
+
violations: List[Violation] = []
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
tree = ast.parse(code)
|
|
59
|
+
except SyntaxError:
|
|
60
|
+
return violations
|
|
61
|
+
|
|
62
|
+
for node in ast.walk(tree):
|
|
63
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
for decorator in node.decorator_list:
|
|
67
|
+
decorator_name = _get_decorator_name(decorator)
|
|
68
|
+
if decorator_name.lower() in SKIP_DECORATOR_NAMES:
|
|
69
|
+
violations.append(
|
|
70
|
+
Violation(
|
|
71
|
+
file=filepath,
|
|
72
|
+
line=node.lineno,
|
|
73
|
+
message=SKIP_DECORATOR_MESSAGE,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return violations
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_decorator_name(decorator: ast.expr) -> str:
|
|
81
|
+
"""Extract the name from a decorator node.
|
|
82
|
+
|
|
83
|
+
Handles both simple decorators (@skip) and attribute decorators
|
|
84
|
+
(@pytest.mark.skip, @unittest.skip).
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
decorator: AST decorator node
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The decorator name (e.g., "skip", "skipif")
|
|
91
|
+
"""
|
|
92
|
+
if isinstance(decorator, ast.Name):
|
|
93
|
+
return decorator.id
|
|
94
|
+
|
|
95
|
+
if isinstance(decorator, ast.Attribute):
|
|
96
|
+
return decorator.attr
|
|
97
|
+
|
|
98
|
+
if isinstance(decorator, ast.Call):
|
|
99
|
+
if isinstance(decorator.func, ast.Name):
|
|
100
|
+
return decorator.func.id
|
|
101
|
+
if isinstance(decorator.func, ast.Attribute):
|
|
102
|
+
return decorator.func.attr
|
|
103
|
+
|
|
104
|
+
return ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_debug_guard_in_dev_scripts(code: str, filepath: str) -> List[Violation]:
|
|
108
|
+
"""Check that Django management commands check settings.DEBUG.
|
|
109
|
+
|
|
110
|
+
Only applies to files in management/commands/ directories.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
code: Python source code to check
|
|
114
|
+
filepath: Path to the file being checked
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of violations found
|
|
118
|
+
"""
|
|
119
|
+
violations: List[Violation] = []
|
|
120
|
+
|
|
121
|
+
normalized_path = filepath.replace("\\", "/")
|
|
122
|
+
if "management/commands/" not in normalized_path:
|
|
123
|
+
return violations
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
tree = ast.parse(code)
|
|
127
|
+
except SyntaxError:
|
|
128
|
+
return violations
|
|
129
|
+
|
|
130
|
+
has_command_class = False
|
|
131
|
+
has_debug_check = False
|
|
132
|
+
command_line = 0
|
|
133
|
+
|
|
134
|
+
for node in ast.walk(tree):
|
|
135
|
+
if isinstance(node, ast.ClassDef):
|
|
136
|
+
if any(
|
|
137
|
+
isinstance(base, ast.Name) and base.id == "BaseCommand"
|
|
138
|
+
for base in node.bases
|
|
139
|
+
):
|
|
140
|
+
has_command_class = True
|
|
141
|
+
command_line = node.lineno
|
|
142
|
+
|
|
143
|
+
for item in node.body:
|
|
144
|
+
if isinstance(item, ast.FunctionDef) and item.name == "handle":
|
|
145
|
+
if _has_debug_guard(item):
|
|
146
|
+
has_debug_check = True
|
|
147
|
+
|
|
148
|
+
if has_command_class and not has_debug_check:
|
|
149
|
+
violations.append(
|
|
150
|
+
Violation(
|
|
151
|
+
file=filepath,
|
|
152
|
+
line=command_line,
|
|
153
|
+
message=DEBUG_CHECK_MESSAGE,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return violations
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _has_debug_guard(func: ast.FunctionDef) -> bool:
|
|
161
|
+
"""Check if a function has a settings.DEBUG guard at the start.
|
|
162
|
+
|
|
163
|
+
Looks for patterns like:
|
|
164
|
+
- if not settings.DEBUG: raise/return
|
|
165
|
+
- if settings.DEBUG: ... else: raise/return
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
func: Function definition node to check
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if function has proper DEBUG guard
|
|
172
|
+
"""
|
|
173
|
+
if not func.body:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
for stmt in func.body[:5]:
|
|
177
|
+
if isinstance(stmt, ast.If):
|
|
178
|
+
test = stmt.test
|
|
179
|
+
|
|
180
|
+
if isinstance(test, ast.UnaryOp) and isinstance(test.op, ast.Not):
|
|
181
|
+
if _is_debug_check(test.operand):
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
if _is_debug_check(test) and stmt.orelse:
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _is_debug_check(node: ast.expr) -> bool:
|
|
191
|
+
"""Check if a node is a settings.DEBUG check.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
node: AST expression node
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
True if node is settings.DEBUG
|
|
198
|
+
"""
|
|
199
|
+
if not isinstance(node, ast.Attribute):
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
if node.attr != "DEBUG":
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
if isinstance(node.value, ast.Name) and node.value.id == "settings":
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def main(file_paths: List[str]) -> int:
|
|
212
|
+
"""Run all safety checks on the given files.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
file_paths: List of file paths to check
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Exit code: 0 if all checks pass, 1 if violations found
|
|
219
|
+
"""
|
|
220
|
+
all_violations: List[Violation] = []
|
|
221
|
+
|
|
222
|
+
for filepath in file_paths:
|
|
223
|
+
path = Path(filepath)
|
|
224
|
+
if not path.exists():
|
|
225
|
+
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
code = path.read_text(encoding="utf-8")
|
|
229
|
+
|
|
230
|
+
violations = check_no_skip_decorators(code, filepath)
|
|
231
|
+
all_violations.extend(violations)
|
|
232
|
+
|
|
233
|
+
violations = check_debug_guard_in_dev_scripts(code, filepath)
|
|
234
|
+
all_violations.extend(violations)
|
|
235
|
+
|
|
236
|
+
for violation in all_violations:
|
|
237
|
+
print(violation)
|
|
238
|
+
|
|
239
|
+
return 1 if all_violations else 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tests for security vulnerability detection."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from security_checks import (
|
|
8
|
+
check_hardcoded_secrets,
|
|
9
|
+
check_sql_injection,
|
|
10
|
+
check_xss_risk,
|
|
11
|
+
)
|
|
12
|
+
from validator_base import Violation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
GOOD_NO_SECRETS = '''
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
def get_api_key():
|
|
19
|
+
return os.environ.get("API_KEY")
|
|
20
|
+
'''
|
|
21
|
+
|
|
22
|
+
BAD_HARDCODED_API_KEY = '''
|
|
23
|
+
API_KEY = "sk-abc123xyz789"
|
|
24
|
+
'''
|
|
25
|
+
|
|
26
|
+
BAD_HARDCODED_PASSWORD = '''
|
|
27
|
+
def connect():
|
|
28
|
+
password = "super_secret_123"
|
|
29
|
+
return password
|
|
30
|
+
'''
|
|
31
|
+
|
|
32
|
+
GOOD_PARAMETERIZED_SQL = '''
|
|
33
|
+
def get_user(user_id):
|
|
34
|
+
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
35
|
+
'''
|
|
36
|
+
|
|
37
|
+
BAD_FSTRING_SQL = '''
|
|
38
|
+
def get_user(user_id):
|
|
39
|
+
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
|
|
40
|
+
'''
|
|
41
|
+
|
|
42
|
+
BAD_FORMAT_SQL = '''
|
|
43
|
+
def get_user(user_id):
|
|
44
|
+
cursor.execute("SELECT * FROM users WHERE id = {}".format(user_id))
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
GOOD_ESCAPED_HTML = '''
|
|
48
|
+
from django.utils.html import escape
|
|
49
|
+
|
|
50
|
+
def render(user_input):
|
|
51
|
+
return escape(user_input)
|
|
52
|
+
'''
|
|
53
|
+
|
|
54
|
+
BAD_MARK_SAFE = '''
|
|
55
|
+
from django.utils.safestring import mark_safe
|
|
56
|
+
|
|
57
|
+
def render(user_input):
|
|
58
|
+
return mark_safe(user_input)
|
|
59
|
+
'''
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestHardcodedSecrets:
|
|
63
|
+
def test_env_variable_passes(self) -> None:
|
|
64
|
+
tree = ast.parse(GOOD_NO_SECRETS)
|
|
65
|
+
violations = check_hardcoded_secrets(tree, "test.py")
|
|
66
|
+
assert violations == []
|
|
67
|
+
|
|
68
|
+
def test_hardcoded_api_key_fails(self) -> None:
|
|
69
|
+
tree = ast.parse(BAD_HARDCODED_API_KEY)
|
|
70
|
+
violations = check_hardcoded_secrets(tree, "test.py")
|
|
71
|
+
assert len(violations) == 1
|
|
72
|
+
assert "API_KEY" in violations[0].message or "secret" in violations[0].message.lower()
|
|
73
|
+
|
|
74
|
+
def test_hardcoded_password_fails(self) -> None:
|
|
75
|
+
tree = ast.parse(BAD_HARDCODED_PASSWORD)
|
|
76
|
+
violations = check_hardcoded_secrets(tree, "test.py")
|
|
77
|
+
assert len(violations) == 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestSqlInjection:
|
|
81
|
+
def test_parameterized_query_passes(self) -> None:
|
|
82
|
+
violations = check_sql_injection(GOOD_PARAMETERIZED_SQL, "test.py")
|
|
83
|
+
assert violations == []
|
|
84
|
+
|
|
85
|
+
def test_fstring_sql_fails(self) -> None:
|
|
86
|
+
violations = check_sql_injection(BAD_FSTRING_SQL, "test.py")
|
|
87
|
+
assert len(violations) == 1
|
|
88
|
+
assert "SQL" in violations[0].message or "injection" in violations[0].message.lower()
|
|
89
|
+
|
|
90
|
+
def test_format_sql_fails(self) -> None:
|
|
91
|
+
violations = check_sql_injection(BAD_FORMAT_SQL, "test.py")
|
|
92
|
+
assert len(violations) == 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestXssRisk:
|
|
96
|
+
def test_escaped_html_passes(self) -> None:
|
|
97
|
+
tree = ast.parse(GOOD_ESCAPED_HTML)
|
|
98
|
+
violations = check_xss_risk(tree, "test.py")
|
|
99
|
+
assert violations == []
|
|
100
|
+
|
|
101
|
+
def test_mark_safe_fails(self) -> None:
|
|
102
|
+
tree = ast.parse(BAD_MARK_SAFE)
|
|
103
|
+
violations = check_xss_risk(tree, "test.py")
|
|
104
|
+
assert len(violations) == 1
|
|
105
|
+
assert "mark_safe" in violations[0].message or "XSS" in violations[0].message
|