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.
Files changed (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/agents/agent-writer.md +157 -0
  4. package/agents/clasp-deployment-orchestrator.md +609 -0
  5. package/agents/clean-coder.md +295 -0
  6. package/agents/code-quality-agent.md +40 -0
  7. package/agents/code-standards-agent.md +93 -0
  8. package/agents/config-centralizer.md +686 -0
  9. package/agents/config-extraction-agent.md +225 -0
  10. package/agents/doc-orchestrator.md +47 -0
  11. package/agents/docs-agent.md +112 -0
  12. package/agents/docx-agent.md +211 -0
  13. package/agents/git-commit-crafter.md +100 -0
  14. package/agents/magic-value-eliminator-agent.md +72 -0
  15. package/agents/mandatory-agent-workflow-agent.md +88 -0
  16. package/agents/parallel-workflow-coordinator.md +779 -0
  17. package/agents/pdf-agent.md +302 -0
  18. package/agents/plan-executor.md +226 -0
  19. package/agents/pr-description-writer.md +87 -0
  20. package/agents/project-context-loader.md +238 -0
  21. package/agents/project-docs-analyzer.md +54 -0
  22. package/agents/project-structure-organizer-agent.md +72 -0
  23. package/agents/readability-review-agent.md +76 -0
  24. package/agents/refactoring-specialist.md +69 -0
  25. package/agents/right-sized-engineer.md +129 -0
  26. package/agents/session-continuity-manager.md +53 -0
  27. package/agents/skill-to-agent-converter.md +371 -0
  28. package/agents/skill-writer-agent.md +470 -0
  29. package/agents/stub-detector-agent.md +140 -0
  30. package/agents/tdd-test-writer.md +62 -0
  31. package/agents/test-data-builder.md +68 -0
  32. package/agents/tooling-builder.md +78 -0
  33. package/agents/user-docs-writer.md +67 -0
  34. package/agents/validation-expert.md +71 -0
  35. package/agents/workflow-visual-documenter.md +82 -0
  36. package/agents/xlsx-agent.md +169 -0
  37. package/bin/install.mjs +256 -0
  38. package/commands/commit.md +28 -0
  39. package/commands/docupdate.md +322 -0
  40. package/commands/implement.md +102 -0
  41. package/commands/initialize.md +91 -0
  42. package/commands/plan.md +63 -0
  43. package/commands/pr-comments.md +47 -0
  44. package/commands/readability-review.md +20 -0
  45. package/commands/review-plan.md +7 -0
  46. package/commands/right-size.md +15 -0
  47. package/commands/stubcheck.md +89 -0
  48. package/commands/sum.md +30 -0
  49. package/docs/CODE_RULES.md +186 -0
  50. package/docs/DJANGO_PATTERNS.md +80 -0
  51. package/docs/REACT_PATTERNS.md +185 -0
  52. package/docs/TEST_QUALITY.md +104 -0
  53. package/hooks/advisory/migration-safety-advisor.py +49 -0
  54. package/hooks/advisory/refactor-guard.py +205 -0
  55. package/hooks/blocking/block-main-commit.py +168 -0
  56. package/hooks/blocking/code-rules-enforcer.py +549 -0
  57. package/hooks/blocking/destructive-command-blocker.py +107 -0
  58. package/hooks/blocking/docker-settings-guard.py +44 -0
  59. package/hooks/blocking/hedging-language-blocker.py +130 -0
  60. package/hooks/blocking/parallel-task-blocker.py +69 -0
  61. package/hooks/blocking/pr-description-enforcer.py +87 -0
  62. package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
  63. package/hooks/blocking/sensitive-file-protector.py +70 -0
  64. package/hooks/blocking/tdd-enforcer.py +62 -0
  65. package/hooks/blocking/test-preflight-check.py +343 -0
  66. package/hooks/blocking/write-existing-file-blocker.py +63 -0
  67. package/hooks/git-hooks/post-commit.py +103 -0
  68. package/hooks/github-action/test_workflow.py +33 -0
  69. package/hooks/hooks.json +246 -0
  70. package/hooks/lifecycle/config-change-guard.py +84 -0
  71. package/hooks/lifecycle/session-end-cleanup.py +59 -0
  72. package/hooks/notification/attention-needed-notify.py +63 -0
  73. package/hooks/notification/claude-notification-handler.py +59 -0
  74. package/hooks/notification/notification_utils.py +206 -0
  75. package/hooks/rewrite-plugin-paths.py +116 -0
  76. package/hooks/session/bulk-edit-reminder.py +30 -0
  77. package/hooks/session/code-rules-reminder.py +97 -0
  78. package/hooks/session/compact-context-reinject.py +39 -0
  79. package/hooks/session/hook-structure-context.py +140 -0
  80. package/hooks/session/plugin-data-dir-cleanup.py +39 -0
  81. package/hooks/validation/code-style-validator.py +145 -0
  82. package/hooks/validation/e2e-test-validator.py +142 -0
  83. package/hooks/validation/hook-format-validator.py +66 -0
  84. package/hooks/validation/mypy_validator.py +180 -0
  85. package/hooks/validators/README.md +125 -0
  86. package/hooks/validators/VALIDATION_REPORT.md +287 -0
  87. package/hooks/validators/__init__.py +19 -0
  88. package/hooks/validators/abbreviation_checks.py +82 -0
  89. package/hooks/validators/code_quality_checks.py +133 -0
  90. package/hooks/validators/comment_checks.py +188 -0
  91. package/hooks/validators/file_structure_checks.py +182 -0
  92. package/hooks/validators/git_checks.py +107 -0
  93. package/hooks/validators/health_check.py +214 -0
  94. package/hooks/validators/magic_value_checks.py +81 -0
  95. package/hooks/validators/mypy_integration.py +52 -0
  96. package/hooks/validators/output_formatter.py +266 -0
  97. package/hooks/validators/pr_reference_checks.py +72 -0
  98. package/hooks/validators/python_antipattern_checks.py +110 -0
  99. package/hooks/validators/python_style_checks.py +364 -0
  100. package/hooks/validators/react_checks.py +90 -0
  101. package/hooks/validators/ruff_integration.py +80 -0
  102. package/hooks/validators/run_all_validators.py +772 -0
  103. package/hooks/validators/security_checks.py +135 -0
  104. package/hooks/validators/test_abbreviation_checks.py +76 -0
  105. package/hooks/validators/test_bad.tsx +7 -0
  106. package/hooks/validators/test_code_quality_checks.py +129 -0
  107. package/hooks/validators/test_file_structure_checks.py +307 -0
  108. package/hooks/validators/test_files/01_basic_component.tsx +10 -0
  109. package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
  110. package/hooks/validators/test_files/03_pure_component.tsx +10 -0
  111. package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
  112. package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
  113. package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
  114. package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
  115. package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
  116. package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
  117. package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
  118. package/hooks/validators/test_files/11_ts_file.ts +10 -0
  119. package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
  120. package/hooks/validators/test_files/13_functional_component.tsx +8 -0
  121. package/hooks/validators/test_files/14_indented_class.tsx +13 -0
  122. package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
  123. package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
  124. package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
  125. package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
  126. package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
  127. package/hooks/validators/test_files/async_views.py +23 -0
  128. package/hooks/validators/test_files/async_with_imports.py +14 -0
  129. package/hooks/validators/test_files/bad_inline_imports.py +37 -0
  130. package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
  131. package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
  132. package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
  133. package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
  134. package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
  135. package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
  136. package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
  137. package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
  138. package/hooks/validators/test_files/not_management_command.py +10 -0
  139. package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
  140. package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
  141. package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
  142. package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
  143. package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
  144. package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
  145. package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
  146. package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
  147. package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
  148. package/hooks/validators/test_files/test_async_functions.py +45 -0
  149. package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
  150. package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
  151. package/hooks/validators/test_git_checks.py +295 -0
  152. package/hooks/validators/test_good.tsx +5 -0
  153. package/hooks/validators/test_health_check.py +57 -0
  154. package/hooks/validators/test_magic_value_checks.py +63 -0
  155. package/hooks/validators/test_mypy_integration.py +27 -0
  156. package/hooks/validators/test_output_formatter.py +150 -0
  157. package/hooks/validators/test_pr_reference_checks.py +41 -0
  158. package/hooks/validators/test_python_antipattern_checks.py +113 -0
  159. package/hooks/validators/test_python_style_checks.py +439 -0
  160. package/hooks/validators/test_react_checks.py +213 -0
  161. package/hooks/validators/test_results.txt +25 -0
  162. package/hooks/validators/test_ruff_integration.py +27 -0
  163. package/hooks/validators/test_run_all_validators.py +228 -0
  164. package/hooks/validators/test_run_all_validators_integration.py +48 -0
  165. package/hooks/validators/test_safety_checks.py +243 -0
  166. package/hooks/validators/test_security_checks.py +105 -0
  167. package/hooks/validators/test_test_safety_checks.py +321 -0
  168. package/hooks/validators/test_todo_checks.py +39 -0
  169. package/hooks/validators/test_type_safety_checks.py +85 -0
  170. package/hooks/validators/test_useless_test_checks.py +55 -0
  171. package/hooks/validators/test_validator_base.py +26 -0
  172. package/hooks/validators/test_verify_paths.py +34 -0
  173. package/hooks/validators/todo_checks.py +59 -0
  174. package/hooks/validators/type_safety_checks.py +101 -0
  175. package/hooks/validators/useless_test_checks.py +92 -0
  176. package/hooks/validators/validator_base.py +19 -0
  177. package/hooks/validators/verify_paths.py +57 -0
  178. package/hooks/workflow/auto-formatter.py +114 -0
  179. package/hooks/workflow/investigation-tracker-reset.py +46 -0
  180. package/package.json +30 -0
  181. package/rules/agent-spawn-protocol.md +47 -0
  182. package/rules/cleanup-temp-files.md +27 -0
  183. package/rules/code-reviews.md +11 -0
  184. package/rules/code-standards.md +43 -0
  185. package/rules/conservative-action.md +20 -0
  186. package/rules/context7.md +12 -0
  187. package/rules/explore-thoroughly.md +27 -0
  188. package/rules/git-workflow.md +42 -0
  189. package/rules/parallel-tools.md +23 -0
  190. package/rules/research-mode.md +23 -0
  191. package/rules/right-sized-engineering.md +28 -0
  192. package/rules/tdd.md +7 -0
  193. package/rules/testing.md +12 -0
  194. package/skills/agent-prompt/SKILL.md +102 -0
  195. package/skills/anthropic-plan/SKILL.md +107 -0
  196. package/skills/everything-search/SKILL.md +144 -0
  197. package/skills/ingest/SKILL.md +40 -0
  198. package/skills/npm-creator/SKILL.md +183 -0
  199. package/skills/pr-review-responder/EXAMPLES.md +590 -0
  200. package/skills/pr-review-responder/PRINCIPLES.md +539 -0
  201. package/skills/pr-review-responder/README.md +209 -0
  202. package/skills/pr-review-responder/SKILL.md +202 -0
  203. package/skills/pr-review-responder/TESTING.md +407 -0
  204. package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
  205. package/skills/pr-review-responder/update_skill.py +297 -0
  206. package/skills/prompt-generator/REFERENCE.md +150 -0
  207. package/skills/prompt-generator/SKILL.md +154 -0
  208. package/skills/readability-review/SKILL.md +127 -0
  209. package/skills/recall/SKILL.md +27 -0
  210. package/skills/remember/SKILL.md +63 -0
  211. package/skills/rule-audit/SKILL.md +307 -0
  212. package/skills/rule-creator/SKILL.md +150 -0
  213. package/skills/skill-writer/REFERENCE.md +246 -0
  214. package/skills/skill-writer/SKILL.md +270 -0
  215. 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