claude-dev-env 2.4.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 -49
- 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 +2 -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 +0 -1
- 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/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- 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 +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- 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/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -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/ruff_integration_constants.py +16 -0
- 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 +1 -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/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- 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 -44
- 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/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- 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_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/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -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/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/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- 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/scripts/test_invoke_code_review.py +0 -966
- 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,5 +1,7 @@
|
|
|
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
|
|
|
@@ -7,6 +9,7 @@ from .ruff_integration import (
|
|
|
7
9
|
RuffResult,
|
|
8
10
|
_config_relative_stdin_filename,
|
|
9
11
|
_parse_fixed_count,
|
|
12
|
+
_staged_ruff_result,
|
|
10
13
|
check_ruff_available,
|
|
11
14
|
run_ruff_check,
|
|
12
15
|
)
|
|
@@ -94,6 +97,92 @@ def test_run_ruff_check_applies_config_resolved_from_config_source_path(
|
|
|
94
97
|
assert "B011" in with_source.output
|
|
95
98
|
|
|
96
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
|
+
|
|
97
186
|
def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
|
|
98
187
|
"""Each reported violation carries a ``path:line:col:`` prefix on its own line.
|
|
99
188
|
|
|
@@ -105,7 +194,8 @@ def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
|
|
|
105
194
|
violating_file = tmp_path / "unused_import_module.py"
|
|
106
195
|
violating_file.write_text("import os\n", encoding="utf-8")
|
|
107
196
|
|
|
108
|
-
|
|
197
|
+
with patch.dict("os.environ", {"FORCE_COLOR": "1"}):
|
|
198
|
+
result = run_ruff_check([violating_file])
|
|
109
199
|
|
|
110
200
|
assert result.passed is False
|
|
111
201
|
location_prefix = f"{violating_file}:1:8:"
|
|
@@ -113,3 +203,4 @@ def test_run_ruff_check_emits_location_prefixed_lines(tmp_path: Path) -> None:
|
|
|
113
203
|
each_line.startswith(location_prefix) and "F401" in each_line
|
|
114
204
|
for each_line in result.output.splitlines()
|
|
115
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,
|
|
@@ -28,6 +30,22 @@ _ASSERT_FALSE_SOURCE = (
|
|
|
28
30
|
"def probe_condition(observed_total: int) -> None:\n"
|
|
29
31
|
" assert False, observed_total\n"
|
|
30
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
|
+
)
|
|
31
49
|
|
|
32
50
|
|
|
33
51
|
def test_validate_proposed_file_threads_ruff_config_from_original_path() -> None:
|
|
@@ -48,6 +66,52 @@ def test_validate_proposed_file_threads_ruff_config_from_original_path() -> None
|
|
|
48
66
|
assert "B011" in ruff_output
|
|
49
67
|
|
|
50
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
|
+
|
|
114
|
+
|
|
51
115
|
class TestFixFlag:
|
|
52
116
|
"""Test --fix flag functionality."""
|
|
53
117
|
|
|
@@ -56,25 +120,11 @@ class TestFixFlag:
|
|
|
56
120
|
with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
|
|
57
121
|
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
58
122
|
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
59
|
-
|
|
60
123
|
mock_get_files.return_value = []
|
|
124
|
+
mock_file.return_value = _passing_mock_result()
|
|
125
|
+
mock_git.return_value = _passing_mock_result()
|
|
61
126
|
|
|
62
|
-
|
|
63
|
-
mock_result.passed = True
|
|
64
|
-
mock_result.name = "Test"
|
|
65
|
-
mock_result.checks = "test"
|
|
66
|
-
mock_result.output = ""
|
|
67
|
-
|
|
68
|
-
mock_file.return_value = mock_result
|
|
69
|
-
mock_git.return_value = mock_result
|
|
70
|
-
|
|
71
|
-
original_argv = sys.argv
|
|
72
|
-
try:
|
|
73
|
-
sys.argv = ["run_all_validators.py", "--fix"]
|
|
74
|
-
result = main()
|
|
75
|
-
assert result == 0
|
|
76
|
-
finally:
|
|
77
|
-
sys.argv = original_argv
|
|
127
|
+
assert _run_main_with_argv(["run_all_validators.py", "--fix"]) == 0
|
|
78
128
|
|
|
79
129
|
def test_fix_flag_calls_fix_python_style(self) -> None:
|
|
80
130
|
"""Verify --fix flag triggers fix_python_style when files exist."""
|
|
@@ -86,29 +136,12 @@ class TestFixFlag:
|
|
|
86
136
|
patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
|
|
87
137
|
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
88
138
|
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
89
|
-
|
|
90
139
|
mock_get_files.return_value = [Path("test.py")]
|
|
91
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()
|
|
92
143
|
|
|
93
|
-
|
|
94
|
-
mock_result.passed = True
|
|
95
|
-
mock_result.name = "Test"
|
|
96
|
-
mock_result.checks = "test"
|
|
97
|
-
mock_result.output = ""
|
|
98
|
-
|
|
99
|
-
mock_style.return_value = mock_result
|
|
100
|
-
mock_test.return_value = mock_result
|
|
101
|
-
mock_react.return_value = mock_result
|
|
102
|
-
mock_comment.return_value = mock_result
|
|
103
|
-
mock_file.return_value = mock_result
|
|
104
|
-
mock_git.return_value = mock_result
|
|
105
|
-
|
|
106
|
-
original_argv = sys.argv
|
|
107
|
-
try:
|
|
108
|
-
sys.argv = ["run_all_validators.py", "--fix"]
|
|
109
|
-
main()
|
|
110
|
-
finally:
|
|
111
|
-
sys.argv = original_argv
|
|
144
|
+
_run_main_with_argv(["run_all_validators.py", "--fix"])
|
|
112
145
|
|
|
113
146
|
mock_fix.assert_called_once()
|
|
114
147
|
|
|
@@ -122,59 +155,71 @@ class TestFixFlag:
|
|
|
122
155
|
patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
|
|
123
156
|
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
124
157
|
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
125
|
-
|
|
126
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()
|
|
127
161
|
|
|
128
|
-
|
|
129
|
-
mock_result.passed = True
|
|
130
|
-
mock_result.name = "Test"
|
|
131
|
-
mock_result.checks = "test"
|
|
132
|
-
mock_result.output = ""
|
|
133
|
-
|
|
134
|
-
mock_style.return_value = mock_result
|
|
135
|
-
mock_test.return_value = mock_result
|
|
136
|
-
mock_react.return_value = mock_result
|
|
137
|
-
mock_comment.return_value = mock_result
|
|
138
|
-
mock_file.return_value = mock_result
|
|
139
|
-
mock_git.return_value = mock_result
|
|
140
|
-
|
|
141
|
-
original_argv = sys.argv
|
|
142
|
-
try:
|
|
143
|
-
sys.argv = ["run_all_validators.py"]
|
|
144
|
-
main()
|
|
145
|
-
finally:
|
|
146
|
-
sys.argv = original_argv
|
|
162
|
+
_run_main_with_argv(["run_all_validators.py"])
|
|
147
163
|
|
|
148
164
|
mock_fix.assert_not_called()
|
|
149
165
|
|
|
150
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
|
+
|
|
151
196
|
class TestGracefulDegradation:
|
|
152
197
|
def test_missing_validator_returns_skipped_result(self) -> None:
|
|
153
198
|
def failing_validator() -> ValidatorResult:
|
|
154
199
|
raise FileNotFoundError("validator.py not found")
|
|
155
200
|
|
|
156
|
-
|
|
201
|
+
skipped_result = run_with_fallback(
|
|
157
202
|
failing_validator,
|
|
158
203
|
"Missing Validator",
|
|
159
204
|
"99",
|
|
160
205
|
)
|
|
161
206
|
|
|
162
|
-
assert
|
|
163
|
-
assert "skipped" in
|
|
164
|
-
assert
|
|
207
|
+
assert skipped_result.skipped is True
|
|
208
|
+
assert "skipped" in skipped_result.output.lower()
|
|
209
|
+
assert skipped_result.passed is False
|
|
165
210
|
|
|
166
211
|
def test_validator_exception_returns_skipped_result(self) -> None:
|
|
167
212
|
def crashing_validator() -> ValidatorResult:
|
|
168
213
|
raise RuntimeError("Unexpected crash")
|
|
169
214
|
|
|
170
|
-
|
|
215
|
+
skipped_result = run_with_fallback(
|
|
171
216
|
crashing_validator,
|
|
172
217
|
"Crashing Validator",
|
|
173
218
|
"99",
|
|
174
219
|
)
|
|
175
220
|
|
|
176
|
-
assert
|
|
177
|
-
assert "skipped" in
|
|
221
|
+
assert skipped_result.skipped is True
|
|
222
|
+
assert "skipped" in skipped_result.output.lower()
|
|
178
223
|
|
|
179
224
|
def test_successful_validator_returns_normal_result(self) -> None:
|
|
180
225
|
def working_validator() -> ValidatorResult:
|
|
@@ -185,14 +230,14 @@ class TestGracefulDegradation:
|
|
|
185
230
|
output="All good",
|
|
186
231
|
)
|
|
187
232
|
|
|
188
|
-
|
|
233
|
+
normal_result = run_with_fallback(
|
|
189
234
|
working_validator,
|
|
190
235
|
"Working",
|
|
191
236
|
"1",
|
|
192
237
|
)
|
|
193
238
|
|
|
194
|
-
assert
|
|
195
|
-
assert
|
|
239
|
+
assert normal_result.skipped is False
|
|
240
|
+
assert normal_result.passed is True
|
|
196
241
|
|
|
197
242
|
|
|
198
243
|
class TestStderrSurfacing:
|
|
@@ -269,7 +314,9 @@ class TestTimingMetrics:
|
|
|
269
314
|
|
|
270
315
|
|
|
271
316
|
class TestVersionHeader:
|
|
272
|
-
def test_print_header_includes_version(
|
|
317
|
+
def test_print_header_includes_version(
|
|
318
|
+
self, capsys: pytest.CaptureFixture[str]
|
|
319
|
+
) -> None:
|
|
273
320
|
print_header()
|
|
274
321
|
captured = capsys.readouterr()
|
|
275
322
|
|