claude-dev-env 2.3.0 → 2.5.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/CLAUDE.md +53 -48
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +3 -4
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/skill-writer-agent.md +84 -0
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +2 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +5 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +107 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +9 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +198 -0
- package/hooks/blocking/code_review_push_gate.py +145 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -155
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
- package/hooks/blocking/test_code_review_push_gate.py +205 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +974 -903
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1014
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +128 -0
- package/hooks/hooks.json +26 -1
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +2 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +257 -25
- package/hooks/validators/run_all_validators.py +223 -19
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +160 -2
- package/hooks/validators/test_run_all_validators.py +140 -68
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -67
- package/rules/durable-post-artifacts.md +7 -0
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +23 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +626 -619
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -421
- package/skills/autoconverge/reference/convergence.md +26 -4
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
- package/skills/autoconverge/workflow/converge.mjs +191 -8
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/skills/team-advisor/SKILL.md +2 -2
- package/scripts/test_invoke_code_review.py +0 -672
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -4,8 +4,6 @@ import ast
|
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
7
|
from .python_antipattern_checks import (
|
|
10
8
|
check_mutable_default_args,
|
|
11
9
|
check_bare_except,
|
|
@@ -198,16 +196,23 @@ def emit_status(message):
|
|
|
198
196
|
'''
|
|
199
197
|
|
|
200
198
|
|
|
201
|
-
def
|
|
202
|
-
|
|
199
|
+
def _named_result(
|
|
200
|
+
file_path: str, content: str, validator_name: str
|
|
201
|
+
) -> ValidatorResult:
|
|
202
|
+
"""Return the named validator result for content proposed at file_path."""
|
|
203
203
|
all_results = validate_proposed_file(file_path, content)
|
|
204
204
|
return next(
|
|
205
205
|
each_result
|
|
206
206
|
for each_result in all_results
|
|
207
|
-
if each_result.name ==
|
|
207
|
+
if each_result.name == validator_name
|
|
208
208
|
)
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
def _antipattern_result_for(file_path: str, content: str) -> ValidatorResult:
|
|
212
|
+
"""Return the Python Anti-patterns result for content proposed at file_path."""
|
|
213
|
+
return _named_result(file_path, content, ANTIPATTERN_VALIDATOR_NAME)
|
|
214
|
+
|
|
215
|
+
|
|
211
216
|
class TestPrintCliExemptionThroughPipeline:
|
|
212
217
|
"""Pin the CLI-marker print exemption through the validate_proposed_file pipeline.
|
|
213
218
|
|
|
@@ -222,3 +227,103 @@ class TestPrintCliExemptionThroughPipeline:
|
|
|
222
227
|
antipattern_result = _antipattern_result_for(CLI_BASENAME, PIPELINE_PRINT_SOURCE)
|
|
223
228
|
assert antipattern_result.passed
|
|
224
229
|
assert PRINT_FINDING_FRAGMENT not in antipattern_result.output
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
SCRIPTS_DIRECTORY_PATH = "packages/foo/scripts/run_job.py"
|
|
233
|
+
NON_EXEMPT_PRODUCTION_PATH = "packages/foo/services/worker.py"
|
|
234
|
+
TEST_FILE_PROPOSED_PATH = "packages/foo/tests/test_helpers.py"
|
|
235
|
+
USELESS_TEST_VALIDATOR_NAME = "Useless Tests"
|
|
236
|
+
USELESS_TEST_SOURCE = '''
|
|
237
|
+
def test_exists():
|
|
238
|
+
assert callable(str)
|
|
239
|
+
'''
|
|
240
|
+
TEST_HELPER_DIRECTORY_PATH = "packages/foo/tests/helper_functions.py"
|
|
241
|
+
NON_TEST_DIRECTORY_HELPER_PATH = "packages/foo/helpers/helper_functions.py"
|
|
242
|
+
CONFIG_DIRECTORY_MAGIC_PATH = "packages/foo/config/threshold_values.py"
|
|
243
|
+
CONFIG_CONSTANTS_DIRECTORY_MAGIC_PATH = (
|
|
244
|
+
"packages/foo/pr_converge_skill_constants/threshold_values.py"
|
|
245
|
+
)
|
|
246
|
+
NON_EXEMPT_MAGIC_PATH = "packages/foo/services/threshold_values.py"
|
|
247
|
+
MAGIC_VALUE_VALIDATOR_NAME = "Magic Values"
|
|
248
|
+
MAGIC_NUMBER_SOURCE = '''
|
|
249
|
+
def scale_amount(quantity):
|
|
250
|
+
return quantity * 199 * 42
|
|
251
|
+
'''
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TestDirectoryExemptionThroughPipeline:
|
|
255
|
+
"""Directory-based exemptions must survive validate_proposed_file staging.
|
|
256
|
+
|
|
257
|
+
Basename-only temp staging drops /scripts/, so print() was falsely denied.
|
|
258
|
+
The real-path unit check allows the same content; the pipeline must match.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def test_scripts_directory_print_passes_through_pipeline(self) -> None:
|
|
262
|
+
tree = ast.parse(PIPELINE_PRINT_SOURCE)
|
|
263
|
+
basename_only = Path(SCRIPTS_DIRECTORY_PATH).name
|
|
264
|
+
|
|
265
|
+
assert len(check_print_in_production(tree, basename_only)) == 1
|
|
266
|
+
assert check_print_in_production(tree, SCRIPTS_DIRECTORY_PATH) == []
|
|
267
|
+
|
|
268
|
+
antipattern_result = _antipattern_result_for(
|
|
269
|
+
SCRIPTS_DIRECTORY_PATH, PIPELINE_PRINT_SOURCE
|
|
270
|
+
)
|
|
271
|
+
assert antipattern_result.passed
|
|
272
|
+
assert PRINT_FINDING_FRAGMENT not in antipattern_result.output
|
|
273
|
+
|
|
274
|
+
def test_non_exempt_production_print_still_fails_through_pipeline(self) -> None:
|
|
275
|
+
antipattern_result = _antipattern_result_for(
|
|
276
|
+
NON_EXEMPT_PRODUCTION_PATH, PIPELINE_PRINT_SOURCE
|
|
277
|
+
)
|
|
278
|
+
assert not antipattern_result.passed
|
|
279
|
+
assert PRINT_FINDING_FRAGMENT in antipattern_result.output
|
|
280
|
+
|
|
281
|
+
def test_test_filename_filtering_still_recognizes_test_path(self) -> None:
|
|
282
|
+
all_results = validate_proposed_file(TEST_FILE_PROPOSED_PATH, USELESS_TEST_SOURCE)
|
|
283
|
+
useless_result = next(
|
|
284
|
+
each_result
|
|
285
|
+
for each_result in all_results
|
|
286
|
+
if each_result.name == USELESS_TEST_VALIDATOR_NAME
|
|
287
|
+
)
|
|
288
|
+
assert not useless_result.passed
|
|
289
|
+
assert "callable" in useless_result.output.lower()
|
|
290
|
+
|
|
291
|
+
antipattern_result = _antipattern_result_for(
|
|
292
|
+
TEST_FILE_PROPOSED_PATH, TEST_FILE_WITH_PRINT
|
|
293
|
+
)
|
|
294
|
+
assert antipattern_result.passed
|
|
295
|
+
assert PRINT_FINDING_FRAGMENT not in antipattern_result.output
|
|
296
|
+
|
|
297
|
+
def test_tests_directory_recognized_without_test_basename(self) -> None:
|
|
298
|
+
exempt_result = _antipattern_result_for(
|
|
299
|
+
TEST_HELPER_DIRECTORY_PATH, PIPELINE_PRINT_SOURCE
|
|
300
|
+
)
|
|
301
|
+
assert exempt_result.passed
|
|
302
|
+
assert PRINT_FINDING_FRAGMENT not in exempt_result.output
|
|
303
|
+
|
|
304
|
+
flagged_result = _antipattern_result_for(
|
|
305
|
+
NON_TEST_DIRECTORY_HELPER_PATH, PIPELINE_PRINT_SOURCE
|
|
306
|
+
)
|
|
307
|
+
assert not flagged_result.passed
|
|
308
|
+
assert PRINT_FINDING_FRAGMENT in flagged_result.output
|
|
309
|
+
|
|
310
|
+
def test_config_directory_magic_value_exempt_through_pipeline(self) -> None:
|
|
311
|
+
exempt_result = _named_result(
|
|
312
|
+
CONFIG_DIRECTORY_MAGIC_PATH, MAGIC_NUMBER_SOURCE, MAGIC_VALUE_VALIDATOR_NAME
|
|
313
|
+
)
|
|
314
|
+
assert exempt_result.passed
|
|
315
|
+
|
|
316
|
+
denied_result = _named_result(
|
|
317
|
+
NON_EXEMPT_MAGIC_PATH, MAGIC_NUMBER_SOURCE, MAGIC_VALUE_VALIDATOR_NAME
|
|
318
|
+
)
|
|
319
|
+
assert not denied_result.passed
|
|
320
|
+
|
|
321
|
+
def test_config_exemption_survives_with_skill_constants_directory_ancestor(
|
|
322
|
+
self,
|
|
323
|
+
) -> None:
|
|
324
|
+
exempt_result = _named_result(
|
|
325
|
+
CONFIG_CONSTANTS_DIRECTORY_MAGIC_PATH,
|
|
326
|
+
MAGIC_NUMBER_SOURCE,
|
|
327
|
+
MAGIC_VALUE_VALIDATOR_NAME,
|
|
328
|
+
)
|
|
329
|
+
assert exempt_result.passed
|
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
"""Tests for ruff integration module."""
|
|
2
2
|
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from unittest.mock import patch
|
|
5
7
|
|
|
6
|
-
from .ruff_integration import
|
|
8
|
+
from .ruff_integration import (
|
|
9
|
+
RuffResult,
|
|
10
|
+
_config_relative_stdin_filename,
|
|
11
|
+
_parse_fixed_count,
|
|
12
|
+
_staged_ruff_result,
|
|
13
|
+
check_ruff_available,
|
|
14
|
+
run_ruff_check,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_parse_fixed_count_reads_the_count_from_the_fixed_line() -> None:
|
|
19
|
+
"""The ``Fixed N`` summary line yields its integer count."""
|
|
20
|
+
assert _parse_fixed_count("Found 3 errors (2 fixed).\nFixed 2 errors\n") == 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_parse_fixed_count_returns_zero_without_a_fixed_line() -> None:
|
|
24
|
+
"""Output with no ``Fixed`` line yields a zero count."""
|
|
25
|
+
assert _parse_fixed_count("All checks passed!\n") == 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_config_relative_stdin_filename_returns_posix_path_under_config_directory() -> None:
|
|
29
|
+
"""A target under the config directory resolves to its config-relative path."""
|
|
30
|
+
config_directory = Path("/repo/hooks")
|
|
31
|
+
target_path = Path("/repo/hooks/validators/run_all_validators.py")
|
|
32
|
+
|
|
33
|
+
relative_filename = _config_relative_stdin_filename(target_path, config_directory)
|
|
34
|
+
|
|
35
|
+
assert relative_filename == "validators/run_all_validators.py"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_config_relative_stdin_filename_falls_back_to_basename_off_tree() -> None:
|
|
39
|
+
"""A target outside the config directory falls back to the bare basename.
|
|
40
|
+
|
|
41
|
+
Path-scoped per-file-ignores cannot apply to a file that does not sit under
|
|
42
|
+
the config directory, so a ``..``-laden relative path is never handed to ruff.
|
|
43
|
+
"""
|
|
44
|
+
config_directory = Path("/repo/hooks")
|
|
45
|
+
target_path = Path("/elsewhere/run_all_validators.py")
|
|
46
|
+
|
|
47
|
+
relative_filename = _config_relative_stdin_filename(target_path, config_directory)
|
|
48
|
+
|
|
49
|
+
assert relative_filename == "run_all_validators.py"
|
|
7
50
|
|
|
8
51
|
|
|
9
52
|
def test_ruff_result_dataclass() -> None:
|
|
@@ -24,9 +67,122 @@ def test_run_ruff_check_returns_passed_for_empty_files() -> None:
|
|
|
24
67
|
"""Test that run_ruff_check passes with no files."""
|
|
25
68
|
result = run_ruff_check([])
|
|
26
69
|
assert result.passed is True
|
|
70
|
+
assert result.fixed_count == 0
|
|
27
71
|
assert "No files" in result.output
|
|
28
72
|
|
|
29
73
|
|
|
74
|
+
def test_run_ruff_check_applies_config_resolved_from_config_source_path(
|
|
75
|
+
tmp_path: Path,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""A given config_source_path resolves the ruff config for a staged copy.
|
|
78
|
+
|
|
79
|
+
A ``[tool.ruff.lint]`` selecting B flags an ``assert False`` (B011) only when
|
|
80
|
+
``config_source_path`` — the original ``.py`` target — resolves it; native
|
|
81
|
+
discovery from the detached staged copy finds no ruff config and passes.
|
|
82
|
+
"""
|
|
83
|
+
ruff_repo = tmp_path / "ruff_repo"
|
|
84
|
+
ruff_repo.mkdir()
|
|
85
|
+
(ruff_repo / "pyproject.toml").write_text(
|
|
86
|
+
"[tool.ruff.lint]\nselect = ['B']\n", encoding="utf-8"
|
|
87
|
+
)
|
|
88
|
+
original_target = ruff_repo / "asserts.py"
|
|
89
|
+
staged_copy = tmp_path / "detached" / "asserts.py"
|
|
90
|
+
staged_copy.parent.mkdir(parents=True)
|
|
91
|
+
staged_copy.write_text("def probe() -> None:\n assert False\n", encoding="utf-8")
|
|
92
|
+
|
|
93
|
+
without_source = run_ruff_check([staged_copy])
|
|
94
|
+
with_source = run_ruff_check([staged_copy], config_source_path=original_target)
|
|
95
|
+
|
|
96
|
+
assert "B011" not in without_source.output
|
|
97
|
+
assert "B011" in with_source.output
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_run_ruff_check_grades_a_staged_copy_holding_non_ascii_text(
|
|
101
|
+
tmp_path: Path,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""A staged copy carrying non-ASCII text reaches ruff intact.
|
|
104
|
+
|
|
105
|
+
The staged shape pipes the copy's text to ruff on stdin, and ruff reads
|
|
106
|
+
stdin as UTF-8. Encoding that pipe with the host locale codec instead
|
|
107
|
+
hands ruff a byte it rejects, so every Python file holding a non-ASCII
|
|
108
|
+
character fails the gate on a host whose locale codec is not UTF-8.
|
|
109
|
+
"""
|
|
110
|
+
ruff_repo = tmp_path / "ruff_repo"
|
|
111
|
+
ruff_repo.mkdir()
|
|
112
|
+
(ruff_repo / "pyproject.toml").write_text("[tool.ruff]\n", encoding="utf-8")
|
|
113
|
+
original_target = ruff_repo / "dashes.py"
|
|
114
|
+
staged_copy = tmp_path / "detached" / "dashes.py"
|
|
115
|
+
staged_copy.parent.mkdir(parents=True)
|
|
116
|
+
staged_copy.write_text('DASH_NOTE = "an em dash — here"\n', encoding="utf-8")
|
|
117
|
+
|
|
118
|
+
result = run_ruff_check([staged_copy], config_source_path=original_target)
|
|
119
|
+
|
|
120
|
+
assert result.passed is True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_staged_ruff_result_pins_utf8_encoding_with_tolerant_decode(
|
|
124
|
+
tmp_path: Path,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""The staged ruff call pins UTF-8 and decodes ruff output tolerantly.
|
|
127
|
+
|
|
128
|
+
::
|
|
129
|
+
|
|
130
|
+
subprocess.run(..., encoding="utf-8", errors="replace", ...)
|
|
131
|
+
ok: locale-independent stdin; a non-UTF-8 byte in ruff output never raises
|
|
132
|
+
flag: encoding omitted -> locale codec; errors omitted -> strict decode
|
|
133
|
+
raises out of subprocess.run on POSIX (fail-open in the gate)
|
|
134
|
+
|
|
135
|
+
The kwargs carry the contract host-independently. The locale fallback the
|
|
136
|
+
integration test above depends on cannot be forced portably, so this guard
|
|
137
|
+
asserts the call itself rather than the emergent behavior.
|
|
138
|
+
"""
|
|
139
|
+
pyproject = tmp_path / "pyproject.toml"
|
|
140
|
+
pyproject.write_text("[tool.ruff]\n", encoding="utf-8")
|
|
141
|
+
captured_keyword_arguments: dict[str, object] = {}
|
|
142
|
+
|
|
143
|
+
def record_call(*positional: object, **keyword: object) -> subprocess.CompletedProcess[str]:
|
|
144
|
+
captured_keyword_arguments.update(keyword)
|
|
145
|
+
return subprocess.CompletedProcess([], 0, stdout="", stderr="")
|
|
146
|
+
|
|
147
|
+
with patch("subprocess.run", side_effect=record_call):
|
|
148
|
+
_staged_ruff_result(pyproject, "probe.py", "x = 1\n")
|
|
149
|
+
|
|
150
|
+
assert captured_keyword_arguments["encoding"] == "utf-8"
|
|
151
|
+
assert captured_keyword_arguments["errors"] == "replace"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_staged_ruff_result_tolerates_non_utf8_bytes_in_ruff_output(
|
|
155
|
+
tmp_path: Path,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""A non-UTF-8 byte in ruff's output is decoded tolerantly, not dropped.
|
|
158
|
+
|
|
159
|
+
::
|
|
160
|
+
|
|
161
|
+
ruff output carries byte 0x92 (a cp1252 curly quote, invalid UTF-8)
|
|
162
|
+
ok: errors="replace" -> output keeps the surrounding text
|
|
163
|
+
flag: errors="strict" -> POSIX raises out of run; Windows drops the
|
|
164
|
+
stream to None and the surrounding text is lost
|
|
165
|
+
|
|
166
|
+
Swaps the ruff argv for a python one-liner emitting the bad byte, so the
|
|
167
|
+
real subprocess decode path runs against real bytes.
|
|
168
|
+
"""
|
|
169
|
+
pyproject = tmp_path / "pyproject.toml"
|
|
170
|
+
pyproject.write_text("[tool.ruff]\n", encoding="utf-8")
|
|
171
|
+
emit_bad_byte_command = [
|
|
172
|
+
sys.executable,
|
|
173
|
+
"-c",
|
|
174
|
+
"import sys; sys.stdout.buffer.write(bytes([0x62, 0x92, 0x63]))",
|
|
175
|
+
]
|
|
176
|
+
staged_module = sys.modules[_staged_ruff_result.__module__]
|
|
177
|
+
with patch.object(
|
|
178
|
+
staged_module, "_staged_ruff_command", return_value=emit_bad_byte_command
|
|
179
|
+
):
|
|
180
|
+
result = _staged_ruff_result(pyproject, "probe.py", "x = 1\n")
|
|
181
|
+
|
|
182
|
+
assert "b" in result.output
|
|
183
|
+
assert "c" in result.output
|
|
184
|
+
|
|
185
|
+
|
|
30
186
|
def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
|
|
31
187
|
"""Each reported violation carries a ``path:line:col:`` prefix on its own line.
|
|
32
188
|
|
|
@@ -38,7 +194,8 @@ def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
|
|
|
38
194
|
violating_file = tmp_path / "unused_import_module.py"
|
|
39
195
|
violating_file.write_text("import os\n", encoding="utf-8")
|
|
40
196
|
|
|
41
|
-
|
|
197
|
+
with patch.dict("os.environ", {"FORCE_COLOR": "1"}):
|
|
198
|
+
result = run_ruff_check([violating_file])
|
|
42
199
|
|
|
43
200
|
assert result.passed is False
|
|
44
201
|
location_prefix = f"{violating_file}:1:8:"
|
|
@@ -46,3 +203,4 @@ def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
|
|
|
46
203
|
each_line.startswith(location_prefix) and "F401" in each_line
|
|
47
204
|
for each_line in result.output.splitlines()
|
|
48
205
|
), result.output
|
|
206
|
+
assert "\x1b[" not in result.output
|
|
@@ -8,9 +8,11 @@ from unittest.mock import MagicMock, patch
|
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
|
+
from .exempt_paths import is_test_file
|
|
11
12
|
from .run_all_validators import (
|
|
12
13
|
ValidatorResult,
|
|
13
14
|
_hooks_subprocess_working_directory_and_environment,
|
|
15
|
+
_temporary_path_preserving_directory_signal,
|
|
14
16
|
add_timing,
|
|
15
17
|
build_json_output,
|
|
16
18
|
create_timing_metrics,
|
|
@@ -20,8 +22,95 @@ from .run_all_validators import (
|
|
|
20
22
|
run_git_checks,
|
|
21
23
|
run_python_style_checks,
|
|
22
24
|
run_with_fallback,
|
|
25
|
+
validate_proposed_file,
|
|
23
26
|
)
|
|
24
27
|
|
|
28
|
+
_VALIDATORS_DIRECTORY = Path(__file__).parent
|
|
29
|
+
_ASSERT_FALSE_SOURCE = (
|
|
30
|
+
"def probe_condition(observed_total: int) -> None:\n"
|
|
31
|
+
" assert False, observed_total\n"
|
|
32
|
+
)
|
|
33
|
+
PRINT_PRODUCTION_CONTENT = 'def run() -> None:\n print("x")\n'
|
|
34
|
+
ALL_PYTHON_TEST_EXEMPT_PATHS = [
|
|
35
|
+
"pkg/test_helpers/worker.py",
|
|
36
|
+
"pkg/conftest.py",
|
|
37
|
+
"pkg/foo_test.py",
|
|
38
|
+
]
|
|
39
|
+
ALL_NON_TEST_PRODUCTION_PATHS = [
|
|
40
|
+
"pkg/worker.py",
|
|
41
|
+
"pkg/latest/worker.py",
|
|
42
|
+
"pkg/protest/worker.py",
|
|
43
|
+
]
|
|
44
|
+
ALL_IS_TEST_FILE_PARITY_PATHS = (
|
|
45
|
+
ALL_PYTHON_TEST_EXEMPT_PATHS
|
|
46
|
+
+ ALL_NON_TEST_PRODUCTION_PATHS
|
|
47
|
+
+ ["pkg/data.spec.helpers/worker.py"]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_validate_proposed_file_threads_ruff_config_from_original_path() -> None:
|
|
52
|
+
"""A non-test staged file is linted under the project ruff config (B selected).
|
|
53
|
+
|
|
54
|
+
::
|
|
55
|
+
|
|
56
|
+
original path .../validators/threading_probe.py, assert False body
|
|
57
|
+
ok: config resolved from the original path -> Ruff output carries B011
|
|
58
|
+
"""
|
|
59
|
+
probe_path = _VALIDATORS_DIRECTORY / "threading_probe.py"
|
|
60
|
+
|
|
61
|
+
all_results = validate_proposed_file(str(probe_path), _ASSERT_FALSE_SOURCE)
|
|
62
|
+
|
|
63
|
+
ruff_output = next(
|
|
64
|
+
each_result.output for each_result in all_results if each_result.name == "Ruff"
|
|
65
|
+
)
|
|
66
|
+
assert "B011" in ruff_output
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _validators_deny_print(file_path: str) -> bool:
|
|
70
|
+
all_results = validate_proposed_file(file_path, PRINT_PRODUCTION_CONTENT)
|
|
71
|
+
return any(not each_result.passed for each_result in all_results)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize("file_path", ALL_PYTHON_TEST_EXEMPT_PATHS)
|
|
75
|
+
def test_staging_keeps_substring_test_exemption(file_path: str) -> None:
|
|
76
|
+
assert is_test_file(file_path) is True
|
|
77
|
+
assert _validators_deny_print(file_path) is False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.parametrize("file_path", ALL_NON_TEST_PRODUCTION_PATHS)
|
|
81
|
+
def test_staging_leaves_non_test_paths_denied(file_path: str) -> None:
|
|
82
|
+
assert is_test_file(file_path) is False
|
|
83
|
+
assert _validators_deny_print(file_path) is True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.parametrize("file_path", ALL_IS_TEST_FILE_PARITY_PATHS)
|
|
87
|
+
def test_staged_path_matches_is_test_file_verdict(
|
|
88
|
+
file_path: str, tmp_path: Path
|
|
89
|
+
) -> None:
|
|
90
|
+
staged_path = _temporary_path_preserving_directory_signal(tmp_path, file_path)
|
|
91
|
+
staged_relative_path = staged_path.relative_to(tmp_path)
|
|
92
|
+
assert is_test_file(str(staged_relative_path)) == is_test_file(file_path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _passing_mock_result() -> MagicMock:
|
|
96
|
+
"""Return a mock validator result that reports a passing check."""
|
|
97
|
+
mock_result = MagicMock()
|
|
98
|
+
mock_result.passed = True
|
|
99
|
+
mock_result.name = "Test"
|
|
100
|
+
mock_result.checks = "test"
|
|
101
|
+
mock_result.output = ""
|
|
102
|
+
return mock_result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _run_main_with_argv(command_line_arguments: list[str]) -> int | None:
|
|
106
|
+
"""Run ``main`` under the given argv and always restore the original argv."""
|
|
107
|
+
original_argv = sys.argv
|
|
108
|
+
try:
|
|
109
|
+
sys.argv = command_line_arguments
|
|
110
|
+
return main()
|
|
111
|
+
finally:
|
|
112
|
+
sys.argv = original_argv
|
|
113
|
+
|
|
25
114
|
|
|
26
115
|
class TestFixFlag:
|
|
27
116
|
"""Test --fix flag functionality."""
|
|
@@ -31,25 +120,11 @@ class TestFixFlag:
|
|
|
31
120
|
with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
|
|
32
121
|
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
33
122
|
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
34
|
-
|
|
35
123
|
mock_get_files.return_value = []
|
|
124
|
+
mock_file.return_value = _passing_mock_result()
|
|
125
|
+
mock_git.return_value = _passing_mock_result()
|
|
36
126
|
|
|
37
|
-
|
|
38
|
-
mock_result.passed = True
|
|
39
|
-
mock_result.name = "Test"
|
|
40
|
-
mock_result.checks = "test"
|
|
41
|
-
mock_result.output = ""
|
|
42
|
-
|
|
43
|
-
mock_file.return_value = mock_result
|
|
44
|
-
mock_git.return_value = mock_result
|
|
45
|
-
|
|
46
|
-
original_argv = sys.argv
|
|
47
|
-
try:
|
|
48
|
-
sys.argv = ["run_all_validators.py", "--fix"]
|
|
49
|
-
result = main()
|
|
50
|
-
assert result == 0
|
|
51
|
-
finally:
|
|
52
|
-
sys.argv = original_argv
|
|
127
|
+
assert _run_main_with_argv(["run_all_validators.py", "--fix"]) == 0
|
|
53
128
|
|
|
54
129
|
def test_fix_flag_calls_fix_python_style(self) -> None:
|
|
55
130
|
"""Verify --fix flag triggers fix_python_style when files exist."""
|
|
@@ -61,29 +136,12 @@ class TestFixFlag:
|
|
|
61
136
|
patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
|
|
62
137
|
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
63
138
|
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
64
|
-
|
|
65
139
|
mock_get_files.return_value = [Path("test.py")]
|
|
66
140
|
mock_fix.return_value = ["test.py"]
|
|
141
|
+
for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file, mock_git):
|
|
142
|
+
each_mock.return_value = _passing_mock_result()
|
|
67
143
|
|
|
68
|
-
|
|
69
|
-
mock_result.passed = True
|
|
70
|
-
mock_result.name = "Test"
|
|
71
|
-
mock_result.checks = "test"
|
|
72
|
-
mock_result.output = ""
|
|
73
|
-
|
|
74
|
-
mock_style.return_value = mock_result
|
|
75
|
-
mock_test.return_value = mock_result
|
|
76
|
-
mock_react.return_value = mock_result
|
|
77
|
-
mock_comment.return_value = mock_result
|
|
78
|
-
mock_file.return_value = mock_result
|
|
79
|
-
mock_git.return_value = mock_result
|
|
80
|
-
|
|
81
|
-
original_argv = sys.argv
|
|
82
|
-
try:
|
|
83
|
-
sys.argv = ["run_all_validators.py", "--fix"]
|
|
84
|
-
main()
|
|
85
|
-
finally:
|
|
86
|
-
sys.argv = original_argv
|
|
144
|
+
_run_main_with_argv(["run_all_validators.py", "--fix"])
|
|
87
145
|
|
|
88
146
|
mock_fix.assert_called_once()
|
|
89
147
|
|
|
@@ -97,59 +155,71 @@ class TestFixFlag:
|
|
|
97
155
|
patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
|
|
98
156
|
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
99
157
|
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
100
|
-
|
|
101
158
|
mock_get_files.return_value = [Path("test.py")]
|
|
159
|
+
for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file, mock_git):
|
|
160
|
+
each_mock.return_value = _passing_mock_result()
|
|
102
161
|
|
|
103
|
-
|
|
104
|
-
mock_result.passed = True
|
|
105
|
-
mock_result.name = "Test"
|
|
106
|
-
mock_result.checks = "test"
|
|
107
|
-
mock_result.output = ""
|
|
108
|
-
|
|
109
|
-
mock_style.return_value = mock_result
|
|
110
|
-
mock_test.return_value = mock_result
|
|
111
|
-
mock_react.return_value = mock_result
|
|
112
|
-
mock_comment.return_value = mock_result
|
|
113
|
-
mock_file.return_value = mock_result
|
|
114
|
-
mock_git.return_value = mock_result
|
|
115
|
-
|
|
116
|
-
original_argv = sys.argv
|
|
117
|
-
try:
|
|
118
|
-
sys.argv = ["run_all_validators.py"]
|
|
119
|
-
main()
|
|
120
|
-
finally:
|
|
121
|
-
sys.argv = original_argv
|
|
162
|
+
_run_main_with_argv(["run_all_validators.py"])
|
|
122
163
|
|
|
123
164
|
mock_fix.assert_not_called()
|
|
124
165
|
|
|
125
166
|
|
|
167
|
+
class TestStagedPathContainment:
|
|
168
|
+
"""Staged validation paths must never escape the temporary root."""
|
|
169
|
+
|
|
170
|
+
def test_parent_traversal_path_stays_inside_temp_root(self, tmp_path: Path) -> None:
|
|
171
|
+
temporary_root = tmp_path / "staging"
|
|
172
|
+
temporary_root.mkdir()
|
|
173
|
+
traversal_path = "scripts/../../../../escaped_evil.py"
|
|
174
|
+
|
|
175
|
+
staged_file = _temporary_path_preserving_directory_signal(
|
|
176
|
+
temporary_root, traversal_path
|
|
177
|
+
)
|
|
178
|
+
staged_file.write_text("payload", encoding="utf-8")
|
|
179
|
+
|
|
180
|
+
assert staged_file.resolve().is_relative_to(temporary_root.resolve())
|
|
181
|
+
assert staged_file.read_text(encoding="utf-8") == "payload"
|
|
182
|
+
|
|
183
|
+
def test_absolute_component_path_stays_inside_temp_root(self, tmp_path: Path) -> None:
|
|
184
|
+
temporary_root = tmp_path / "staging"
|
|
185
|
+
temporary_root.mkdir()
|
|
186
|
+
absolute_style_path = "/etc/cron.d/evil.py"
|
|
187
|
+
|
|
188
|
+
staged_file = _temporary_path_preserving_directory_signal(
|
|
189
|
+
temporary_root, absolute_style_path
|
|
190
|
+
)
|
|
191
|
+
staged_file.write_text("payload", encoding="utf-8")
|
|
192
|
+
|
|
193
|
+
assert staged_file.resolve().is_relative_to(temporary_root.resolve())
|
|
194
|
+
|
|
195
|
+
|
|
126
196
|
class TestGracefulDegradation:
|
|
127
197
|
def test_missing_validator_returns_skipped_result(self) -> None:
|
|
128
198
|
def failing_validator() -> ValidatorResult:
|
|
129
199
|
raise FileNotFoundError("validator.py not found")
|
|
130
200
|
|
|
131
|
-
|
|
201
|
+
skipped_result = run_with_fallback(
|
|
132
202
|
failing_validator,
|
|
133
203
|
"Missing Validator",
|
|
134
204
|
"99",
|
|
135
205
|
)
|
|
136
206
|
|
|
137
|
-
assert
|
|
138
|
-
assert "skipped" in
|
|
139
|
-
assert
|
|
207
|
+
assert skipped_result.skipped is True
|
|
208
|
+
assert "skipped" in skipped_result.output.lower()
|
|
209
|
+
assert skipped_result.passed is False
|
|
140
210
|
|
|
141
211
|
def test_validator_exception_returns_skipped_result(self) -> None:
|
|
142
212
|
def crashing_validator() -> ValidatorResult:
|
|
143
213
|
raise RuntimeError("Unexpected crash")
|
|
144
214
|
|
|
145
|
-
|
|
215
|
+
skipped_result = run_with_fallback(
|
|
146
216
|
crashing_validator,
|
|
147
217
|
"Crashing Validator",
|
|
148
218
|
"99",
|
|
149
219
|
)
|
|
150
220
|
|
|
151
|
-
assert
|
|
152
|
-
assert "skipped" in
|
|
221
|
+
assert skipped_result.skipped is True
|
|
222
|
+
assert "skipped" in skipped_result.output.lower()
|
|
153
223
|
|
|
154
224
|
def test_successful_validator_returns_normal_result(self) -> None:
|
|
155
225
|
def working_validator() -> ValidatorResult:
|
|
@@ -160,14 +230,14 @@ class TestGracefulDegradation:
|
|
|
160
230
|
output="All good",
|
|
161
231
|
)
|
|
162
232
|
|
|
163
|
-
|
|
233
|
+
normal_result = run_with_fallback(
|
|
164
234
|
working_validator,
|
|
165
235
|
"Working",
|
|
166
236
|
"1",
|
|
167
237
|
)
|
|
168
238
|
|
|
169
|
-
assert
|
|
170
|
-
assert
|
|
239
|
+
assert normal_result.skipped is False
|
|
240
|
+
assert normal_result.passed is True
|
|
171
241
|
|
|
172
242
|
|
|
173
243
|
class TestStderrSurfacing:
|
|
@@ -244,7 +314,9 @@ class TestTimingMetrics:
|
|
|
244
314
|
|
|
245
315
|
|
|
246
316
|
class TestVersionHeader:
|
|
247
|
-
def test_print_header_includes_version(
|
|
317
|
+
def test_print_header_includes_version(
|
|
318
|
+
self, capsys: pytest.CaptureFixture[str]
|
|
319
|
+
) -> None:
|
|
248
320
|
print_header()
|
|
249
321
|
captured = capsys.readouterr()
|
|
250
322
|
|