claude-dev-env 2.7.1 → 2.9.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 +7 -1
- package/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/clean-coder.md +9 -19
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +335 -12
- package/docs/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +2 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/team-advisor-skill.md +14 -8
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/output-styles/CLAUDE.md +17 -0
- package/output-styles/caveman-agent.md +37 -0
- package/package.json +2 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/rules/code-standards.md +33 -7
- package/rules/eli11-replies.md +1 -1
- package/scripts/CLAUDE.md +3 -3
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -0
- package/scripts/spawn_grok_batch.py +591 -10
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +396 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/CLAUDE.md +4 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +15 -9
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/runner-selection.md +40 -0
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
- package/skills/e-code-review/scripts/grok_code_review.py +221 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +10 -0
- package/skills/orchestrator/SKILL.md +9 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -41
- package/system-prompts/software-engineer.xml +6 -3
- package/skills/test_markdown_link_integrity.py +0 -107
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
"""Tests for the piped-pytest Bash blocker.
|
|
2
|
+
|
|
3
|
+
The deny cases each carry a pytest spelling whose output feeds a pipe; the allow
|
|
4
|
+
cases each carry a pytest or pipe shape that keeps the pytest exit code. The
|
|
5
|
+
dispatcher case drives the payload through the hosted-hook chain, so it holds
|
|
6
|
+
only while the roster carries the segment.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import shlex
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
_BLOCKING_DIR = Path(__file__).resolve().parent
|
|
20
|
+
_HOOKS_DIR = str(_BLOCKING_DIR.parent)
|
|
21
|
+
if _HOOKS_DIR not in sys.path:
|
|
22
|
+
sys.path.insert(0, _HOOKS_DIR)
|
|
23
|
+
_BLOCKING_DIR_TEXT = str(_BLOCKING_DIR)
|
|
24
|
+
if _BLOCKING_DIR_TEXT not in sys.path:
|
|
25
|
+
sys.path.insert(0, _BLOCKING_DIR_TEXT)
|
|
26
|
+
|
|
27
|
+
from hooks_constants.piped_pytest_blocker_constants import ( # noqa: E402
|
|
28
|
+
CORRECTIVE_MESSAGE,
|
|
29
|
+
)
|
|
30
|
+
from hooks_constants.shell_command_segments import split_into_segments # noqa: E402
|
|
31
|
+
from piped_pytest_blocker import ( # noqa: E402
|
|
32
|
+
find_piped_pytest_violation,
|
|
33
|
+
segment_runs_pytest,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
SCRIPT_PATH = _BLOCKING_DIR / "piped_pytest_blocker.py"
|
|
37
|
+
DISPATCHER_PATH = _BLOCKING_DIR / "bash_pre_tool_use_dispatcher.py"
|
|
38
|
+
QUOTED_PIPE_COMMAND = 'pytest -k "a|b"'
|
|
39
|
+
SHARED_SPLITTER_SEGMENTS_FOR_QUOTED_PIPE = [["pytest", "-k", "a"], ["b"]]
|
|
40
|
+
|
|
41
|
+
ALL_PIPED_PYTEST_COMMANDS = [
|
|
42
|
+
"pytest tests | tee run.log",
|
|
43
|
+
"python -m pytest tests | head -50",
|
|
44
|
+
r"C:\Python313\python.exe -m pytest tests | tee run.log",
|
|
45
|
+
"python -m pytest tests 2>&1 | tee run.log",
|
|
46
|
+
"cd packages/claude-dev-env && pytest | cat",
|
|
47
|
+
"(python -m pytest tests) | tee run.log",
|
|
48
|
+
"(python -m pytest tests)|tee run.log",
|
|
49
|
+
"(python -m pytest tests)|&tee run.log",
|
|
50
|
+
"(python -m pytest tests)|(tee run.log)",
|
|
51
|
+
"(python -m pytest tests)|&(tee run.log)",
|
|
52
|
+
"((python -m pytest tests))|(tee run.log)",
|
|
53
|
+
'cmd /c "python -m pytest tests | tee run.log"',
|
|
54
|
+
'cmd.exe /C "pytest tests | tee run.log"',
|
|
55
|
+
"cmd /c python -m pytest tests | tee run.log",
|
|
56
|
+
"cmd.exe /c python -m pytest | cat",
|
|
57
|
+
"bash -c python -m pytest tests | tee run.log",
|
|
58
|
+
"pwsh -Command python -m pytest tests | tee run.log",
|
|
59
|
+
"pytest tests#tag | tee run.log",
|
|
60
|
+
"python -m pytest tests --junitxml=r#1.xml | tee run.log",
|
|
61
|
+
"bash -c 'pytest | tee run.log'",
|
|
62
|
+
"bash -c 'pytest tests | tee x'",
|
|
63
|
+
"python -mpytest tests | tee run.log",
|
|
64
|
+
"time pytest tests | tee run.log",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
ALL_BRACE_GROUP_PYTEST_COMMANDS = [
|
|
68
|
+
"{ pytest tests; } | tee run.log",
|
|
69
|
+
"{ python -m pytest tests; }|tee run.log",
|
|
70
|
+
"{ echo start; pytest tests; } | tee run.log",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
ALL_KEYWORD_COMPOUND_PYTEST_COMMANDS = [
|
|
74
|
+
"if true; then pytest tests; fi | tee run.log",
|
|
75
|
+
"while true; do pytest tests; done | tee run.log",
|
|
76
|
+
"for x in a; do pytest tests; done | tee run.log",
|
|
77
|
+
"if false; then echo skip; else pytest tests; fi | tee run.log",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
ALL_EARLIER_BRANCH_PYTEST_COMMANDS = [
|
|
81
|
+
"if true; then pytest tests; else echo skip; fi | tee run.log",
|
|
82
|
+
"if true; then pytest tests; elif false; then echo b; fi | tee run.log",
|
|
83
|
+
"if true; then echo start; pytest tests; else echo skip; fi | tee run.log",
|
|
84
|
+
"if a; then echo x; elif b; then pytest tests; else echo y; fi | tee run.log",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
ALL_REDIRECTED_CLOSER_PYTEST_COMMANDS = [
|
|
88
|
+
"if true; then pytest tests; fi 2>&1 | tee run.log",
|
|
89
|
+
"if true; then pytest tests; fi > out.log | tee run.log",
|
|
90
|
+
"while read x; do pytest $x; done < list | tee run.log",
|
|
91
|
+
"{ pytest tests; } 2>&1 | tee run.log",
|
|
92
|
+
"{ pytest tests; } >> out.log | tee run.log",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
SCRIPT_OPERAND_CARRYING_A_MODULE_FLAG = "python myscript.py -m pytest | tee run.log"
|
|
96
|
+
ALL_INTERPRETER_MODULE_RUN_PYTEST_COMMANDS = [
|
|
97
|
+
"python -m pytest tests | tee run.log",
|
|
98
|
+
"python -mpytest tests | tee run.log",
|
|
99
|
+
"python -m pytest tests --junitxml=r#1.xml | tee run.log",
|
|
100
|
+
"python -X dev -m pytest tests | tee run.log",
|
|
101
|
+
"python -W ignore -m pytest tests | tee run.log",
|
|
102
|
+
"python -Xdev -m pytest tests | tee run.log",
|
|
103
|
+
"python -u -m pytest tests | tee run.log",
|
|
104
|
+
"python -B -m pytest tests | tee run.log",
|
|
105
|
+
"python -OO -m pytest tests | tee run.log",
|
|
106
|
+
"python --check-hash-based-pycs always -m pytest tests | tee run.log",
|
|
107
|
+
"pypy3 --jit off -m pytest tests | tee run.log",
|
|
108
|
+
"sudo python -m pytest tests | tee run.log",
|
|
109
|
+
"uv run python -m pytest tests | tee run.log",
|
|
110
|
+
"uvx python -m pytest tests | tee run.log",
|
|
111
|
+
"coverage run -m pytest tests | tee run.log",
|
|
112
|
+
"poetry run python -m pytest tests | tee run.log",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
WRAPPER_CARRYING_THE_ONLY_PIPE = "bash -c 'python -m pytest tests | tee wrapped.log'"
|
|
116
|
+
|
|
117
|
+
ALL_WINDOWS_SHIM_PYTEST_COMMANDS = [
|
|
118
|
+
"pytest.bat tests | tee run.log",
|
|
119
|
+
"pytest.cmd tests | tee run.log",
|
|
120
|
+
"py.test.bat tests | tee run.log",
|
|
121
|
+
"py.test.cmd tests | tee run.log",
|
|
122
|
+
"python.bat -m pytest tests | tee run.log",
|
|
123
|
+
"python.cmd -m pytest tests | tee run.log",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
ALL_PASS_THROUGH_WRAPPER_PYTEST_COMMANDS = [
|
|
127
|
+
"sudo pytest tests | tee run.log",
|
|
128
|
+
"uv run pytest tests | tee run.log",
|
|
129
|
+
"poetry run pytest tests | tee run.log",
|
|
130
|
+
"pipenv run pytest tests | tee run.log",
|
|
131
|
+
"uv run python -m pytest tests | tee run.log",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
ALL_FLAGGED_WRAPPER_PYTEST_COMMANDS = [
|
|
135
|
+
"uv run --frozen pytest tests | tee run.log",
|
|
136
|
+
"uv run --no-sync pytest tests | tee run.log",
|
|
137
|
+
"poetry run --no-plugins pytest tests | tee run.log",
|
|
138
|
+
"uv run --python 3.13 pytest tests | tee run.log",
|
|
139
|
+
"uv run -- pytest tests | tee run.log",
|
|
140
|
+
"sudo -n pytest tests | tee run.log",
|
|
141
|
+
"sudo -u ci pytest tests | tee run.log",
|
|
142
|
+
"sudo -- pytest tests | tee run.log",
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
ALL_CLUSTERED_SHORT_OPTION_PYTEST_COMMANDS = [
|
|
146
|
+
"sudo -nu ci pytest tests | tee run.log",
|
|
147
|
+
"sudo -nuci pytest tests | tee run.log",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
ALL_SHELL_OPTION_VALUE_PYTEST_COMMANDS = [
|
|
151
|
+
"bash -o pipefail -c 'pytest tests | tee run.log'",
|
|
152
|
+
"bash -euo pipefail -c 'pytest tests | tee run.log'",
|
|
153
|
+
"pwsh -ExecutionPolicy Bypass -Command 'pytest tests | tee run.log'",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
ALL_CLUSTERED_STRING_EXEC_PYTEST_COMMANDS = [
|
|
157
|
+
"bash -euc 'pytest tests | tee run.log'",
|
|
158
|
+
"bash -ec 'pytest tests | tee run.log'",
|
|
159
|
+
"bash -xc 'pytest tests | tee run.log'",
|
|
160
|
+
"bash -ceu 'pytest tests | tee run.log'",
|
|
161
|
+
"sh -ec 'pytest tests | tee run.log'",
|
|
162
|
+
"bash -euoc pipefail 'pytest tests | tee run.log'",
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
ALL_COVERAGE_RUN_PYTEST_COMMANDS = [
|
|
166
|
+
"coverage run -m pytest tests | tee run.log",
|
|
167
|
+
"coverage run pytest tests | tee run.log",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
ALL_TOOL_RUNNER_WRAPPER_PYTEST_COMMANDS = [
|
|
171
|
+
"uvx pytest tests | tee run.log",
|
|
172
|
+
"uvx --from pytest-xdist pytest tests | tee run.log",
|
|
173
|
+
"uv tool run pytest tests | tee run.log",
|
|
174
|
+
"uv tool run --from pytest-xdist pytest tests | tee run.log",
|
|
175
|
+
"pdm run pytest tests | tee run.log",
|
|
176
|
+
"hatch run pytest tests | tee run.log",
|
|
177
|
+
"rye run pytest tests | tee run.log",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
ALL_ALTERNATE_INTERPRETER_PYTEST_COMMANDS = [
|
|
181
|
+
"python3.13t -m pytest tests | tee run.log",
|
|
182
|
+
"pypy3 -m pytest tests | tee run.log",
|
|
183
|
+
"pythonw -m pytest tests | tee run.log",
|
|
184
|
+
"pyw -m pytest tests | tee run.log",
|
|
185
|
+
"pythonw3.13.exe -m pytest tests | tee run.log",
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
MULTI_LINE_SUBSHELL_PYTEST_COMMAND = "(\npython -m pytest tests\n) | tee run.log"
|
|
189
|
+
SUBSHELL_WRITING_A_PIPED_PYTEST_HEREDOC = (
|
|
190
|
+
"(\ncat > run.sh <<'EOF'\npytest tests | tee out.log\nEOF\n) | tee wrote.log"
|
|
191
|
+
)
|
|
192
|
+
SUBSHELL_CARRYING_A_COMMENT_LINE = "(\n# the fast run\npython -m pytest tests\n) | tee run.log"
|
|
193
|
+
SUBSHELL_CARRYING_A_TRAILING_COMMENT = "(\npython -m pytest tests # fast\n) | tee run.log"
|
|
194
|
+
COMMENT_CARRYING_A_STRAY_OPEN_PAREN = "# rerun the failing case (see the note\npytest | tee run.log"
|
|
195
|
+
COMMENT_NAMING_A_HEREDOC_OPENER = "# write it with <<EOF next time\npytest tests | tee run.log"
|
|
196
|
+
TRAILING_COMMENT_NAMING_A_HEREDOC_OPENER = (
|
|
197
|
+
"echo hi # cat <<EOF\npython -m pytest tests | tee run.log"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
ALL_NESTED_MODULE_PYTEST_COMMANDS = [
|
|
201
|
+
"python -m coverage run -m pytest tests | tee run.log",
|
|
202
|
+
"python -m debugpy -m pytest tests | tee out.log",
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
ALL_WRAPPED_PYTEST_COMMANDS_PIPED_FROM_OUTSIDE = [
|
|
206
|
+
"bash -c 'pytest tests' | tee run.log",
|
|
207
|
+
"pwsh -Command 'pytest tests' | tee run.log",
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
ALL_HEREDOC_OPENER_SPELLINGS = ["<<EOF", "<<'EOF'", '<<"EOF"', "<<-EOF"]
|
|
211
|
+
HEREDOC_SCRIPT_TEMPLATE = "cat > run.sh {opener}\npytest tests | tee out.log\nEOF"
|
|
212
|
+
|
|
213
|
+
ALL_WIDE_HEREDOC_DELIMITERS = ["END-OF-TEST", "EOF-2.1", "run.sh-1", "EOF2"]
|
|
214
|
+
ALL_WIDE_HEREDOC_SCRIPT_TEMPLATES = [
|
|
215
|
+
"cat > run.sh <<{delimiter}\npytest tests | tee out.log\n{delimiter}",
|
|
216
|
+
"cat > run.sh <<'{delimiter}'\npytest tests | tee out.log\n{delimiter}",
|
|
217
|
+
'cat > run.sh <<"{delimiter}"\npytest tests | tee out.log\n{delimiter}',
|
|
218
|
+
"cat > run.sh <<-{delimiter}\npytest tests | tee out.log\n{delimiter}",
|
|
219
|
+
"cat > run.sh <<\\{delimiter}\npytest tests | tee out.log\n{delimiter}",
|
|
220
|
+
]
|
|
221
|
+
HERE_STRING_ABOVE_A_PIPED_PYTEST_RUN = "cat file <<<word\npytest tests | tee run.log"
|
|
222
|
+
COMMAND_AFTER_A_HEREDOC = "cat > run.sh <<'EOF'\necho hi\nEOF\npytest tests | tee out.log"
|
|
223
|
+
|
|
224
|
+
HEREDOC_BODY_LINE_SPACED_LIKE_ITS_TERMINATOR = (
|
|
225
|
+
"cat > run.sh <<EOF\n EOF \npytest tests | tee out.log\nEOF"
|
|
226
|
+
)
|
|
227
|
+
LIVE_RUN_BELOW_A_SPACED_TERMINATOR_LOOKALIKE = (
|
|
228
|
+
f"{HEREDOC_BODY_LINE_SPACED_LIKE_ITS_TERMINATOR}\npytest tests | tee live.log"
|
|
229
|
+
)
|
|
230
|
+
LIVE_RUN_BELOW_A_TAB_INDENTED_TERMINATOR = (
|
|
231
|
+
"cat > run.sh <<-EOF\npytest tests | tee out.log\n\tEOF\npytest tests | tee live.log"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
ALL_LINE_CONTINUATION_TERMINATORS = ["\r\n", "\r", "\n"]
|
|
235
|
+
|
|
236
|
+
ALL_EXIT_CODE_PRESERVING_COMMANDS = [
|
|
237
|
+
"pytest tests",
|
|
238
|
+
"python -m pytest tests > run.log 2>&1",
|
|
239
|
+
"git status --short | head -20",
|
|
240
|
+
"cat ids.txt | pytest --stdin",
|
|
241
|
+
"pytest tests && echo done | tee run.log",
|
|
242
|
+
"python -m mypy . | tee types.log",
|
|
243
|
+
"pytest tests -q\ngit status | head",
|
|
244
|
+
"""bash -c 'pytest -k "a|b"'""",
|
|
245
|
+
"pytest tests >| run.log",
|
|
246
|
+
"(pytest tests)||echo failed",
|
|
247
|
+
"pytest tests # | tee run.log",
|
|
248
|
+
"cmd /c python -m mypy . | tee types.log",
|
|
249
|
+
"cmd /c python -m pytest tests > run.log 2>&1",
|
|
250
|
+
"bash -c 'git status' | tee status.log",
|
|
251
|
+
"bash scripts/ci.sh -c 'pytest tests' | tee run.log",
|
|
252
|
+
"sh scripts/ci.sh -lc 'pytest tests' | tee run.log",
|
|
253
|
+
"cp file{a,b}.txt dst | tee log",
|
|
254
|
+
"{ git status; } | tee run.log",
|
|
255
|
+
"sudo apt update | tee log",
|
|
256
|
+
"uv sync | tee build.log",
|
|
257
|
+
"poetry run mypy . | tee types.log",
|
|
258
|
+
"uv run --with pytest mypy . | tee types.log",
|
|
259
|
+
"uv run --directory sub mypy . | tee types.log",
|
|
260
|
+
"sudo -u pytest apt update | tee log",
|
|
261
|
+
"bash -- -c 'pytest tests' | tee run.log",
|
|
262
|
+
"bash scripts/ci.sh -c 'pytest tests'",
|
|
263
|
+
"pwsh -File run.ps1 -c 'pytest tests' | tee run.log",
|
|
264
|
+
"sudo -nu ci apt update | tee log",
|
|
265
|
+
"sudo -nuci apt update | tee log",
|
|
266
|
+
"coverage run -m mypy . | tee types.log",
|
|
267
|
+
"coverage run --source pytest -m mypy . | tee types.log",
|
|
268
|
+
"pyright -m pytest . | tee out.log",
|
|
269
|
+
"bash -Cu scripts/ci.sh | tee run.log",
|
|
270
|
+
"bash -eux scripts/ci.sh | tee run.log",
|
|
271
|
+
"pwsh -NonInteractive -File run.ps1 | tee run.log",
|
|
272
|
+
"uvx mypy . | tee types.log",
|
|
273
|
+
"uv tool install pytest | tee install.log",
|
|
274
|
+
"pdm run mypy . | tee types.log",
|
|
275
|
+
"ls done | tee log",
|
|
276
|
+
"tar -cf out.tar done | tee log",
|
|
277
|
+
"echo fi | tee log",
|
|
278
|
+
"echo esac | tee log",
|
|
279
|
+
"ls do | tee log",
|
|
280
|
+
"ls else | tee log",
|
|
281
|
+
"if pytest tests; then echo ok; fi | tee run.log",
|
|
282
|
+
"if cd repo && pytest tests; then echo ok; fi | tee run.log",
|
|
283
|
+
"until pytest tests; do echo retry; done | tee run.log",
|
|
284
|
+
"while pytest tests; do echo again; done | tee run.log",
|
|
285
|
+
"if a; then echo x; elif pytest tests; then echo b; fi | tee run.log",
|
|
286
|
+
"if true; then pytest tests; echo after; else echo skip; fi | tee run.log",
|
|
287
|
+
"if a; then pytest tests; fi; if b; then echo c; fi | tee run.log",
|
|
288
|
+
"{ pytest tests; }; if b; then echo c; else echo d; fi | tee run.log",
|
|
289
|
+
"pytest tests; { echo a; } | tee run.log",
|
|
290
|
+
"python -c 'import os' -m pytest tests | tee run.log",
|
|
291
|
+
"python -Wu ignore -m pytest tests | tee run.log",
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _run_process(script_path: Path, payload: dict) -> subprocess.CompletedProcess[str]:
|
|
296
|
+
return subprocess.run(
|
|
297
|
+
[sys.executable, str(script_path)],
|
|
298
|
+
input=json.dumps(payload),
|
|
299
|
+
text=True,
|
|
300
|
+
capture_output=True,
|
|
301
|
+
check=False,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _bash_payload(command: str) -> dict:
|
|
306
|
+
return {"tool_name": "Bash", "tool_input": {"command": command}}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _deny_reason_from_stdout(stdout_text: str) -> str:
|
|
310
|
+
parsed_payload = json.loads(stdout_text)
|
|
311
|
+
hook_specific = parsed_payload["hookSpecificOutput"]
|
|
312
|
+
assert hook_specific["permissionDecision"] == "deny"
|
|
313
|
+
return hook_specific["permissionDecisionReason"]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@pytest.mark.parametrize("each_command", ALL_PIPED_PYTEST_COMMANDS)
|
|
317
|
+
def test_denies_a_pytest_run_feeding_a_pipe(each_command: str) -> None:
|
|
318
|
+
"""Each covered pytest spelling returns the deny message when it feeds a pipe."""
|
|
319
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@pytest.mark.parametrize("each_command", ALL_EXIT_CODE_PRESERVING_COMMANDS)
|
|
323
|
+
def test_allows_a_command_that_keeps_the_pytest_exit_code(each_command: str) -> None:
|
|
324
|
+
"""Bare pytest, redirection-only pytest, and unrelated pipes stay allowed."""
|
|
325
|
+
assert find_piped_pytest_violation(each_command) is None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@pytest.mark.parametrize("each_command", ALL_NESTED_MODULE_PYTEST_COMMANDS)
|
|
329
|
+
def test_denies_pytest_named_by_a_later_module_flag(each_command: str) -> None:
|
|
330
|
+
"""A runner module ahead of ``-m pytest`` still leaves pytest as the run."""
|
|
331
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@pytest.mark.parametrize("each_command", ALL_WRAPPED_PYTEST_COMMANDS_PIPED_FROM_OUTSIDE)
|
|
335
|
+
def test_denies_a_wrapped_pytest_run_piped_from_outside(each_command: str) -> None:
|
|
336
|
+
"""A shell wrapper exits with pytest's code, so a pipe after it hides a failure."""
|
|
337
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@pytest.mark.parametrize("each_opener", ALL_HEREDOC_OPENER_SPELLINGS)
|
|
341
|
+
def test_allows_a_heredoc_that_writes_a_piped_pytest_line(each_opener: str) -> None:
|
|
342
|
+
"""A heredoc body is script text this call writes, not a command it runs."""
|
|
343
|
+
script_writing_command = HEREDOC_SCRIPT_TEMPLATE.format(opener=each_opener)
|
|
344
|
+
assert find_piped_pytest_violation(script_writing_command) is None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@pytest.mark.parametrize("each_template", ALL_WIDE_HEREDOC_SCRIPT_TEMPLATES)
|
|
348
|
+
@pytest.mark.parametrize("each_delimiter", ALL_WIDE_HEREDOC_DELIMITERS)
|
|
349
|
+
def test_allows_a_heredoc_whose_delimiter_carries_a_hyphen_digit_or_dot(
|
|
350
|
+
each_delimiter: str, each_template: str
|
|
351
|
+
) -> None:
|
|
352
|
+
"""A shell takes any word as a delimiter, so the body below one stays script text."""
|
|
353
|
+
script_writing_command = each_template.format(delimiter=each_delimiter)
|
|
354
|
+
assert find_piped_pytest_violation(script_writing_command) is None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@pytest.mark.parametrize("each_command", ALL_BRACE_GROUP_PYTEST_COMMANDS)
|
|
358
|
+
def test_denies_a_brace_group_whose_last_command_is_pytest(each_command: str) -> None:
|
|
359
|
+
"""A brace group exits with its last command's code, so the pipe after it hides it."""
|
|
360
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@pytest.mark.parametrize("each_command", ALL_KEYWORD_COMPOUND_PYTEST_COMMANDS)
|
|
364
|
+
def test_denies_a_keyword_compound_whose_last_command_is_pytest(each_command: str) -> None:
|
|
365
|
+
"""``fi`` and ``done`` end a compound, so the pipe after one reads pytest's code."""
|
|
366
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@pytest.mark.parametrize("each_command", ALL_EARLIER_BRANCH_PYTEST_COMMANDS)
|
|
370
|
+
def test_denies_a_compound_whose_earlier_branch_runs_pytest(each_command: str) -> None:
|
|
371
|
+
"""A branch other than the last one still runs, so its exit code reaches the pipe."""
|
|
372
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@pytest.mark.parametrize("each_command", ALL_REDIRECTED_CLOSER_PYTEST_COMMANDS)
|
|
376
|
+
def test_denies_a_compound_whose_closer_carries_a_redirection(each_command: str) -> None:
|
|
377
|
+
"""A redirection binds to the compound, so the closer before it still closes it."""
|
|
378
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_allows_a_module_flag_that_belongs_to_a_script_operand() -> None:
|
|
382
|
+
"""A script path ends the interpreter's options, so its own ``-m pytest`` runs nothing."""
|
|
383
|
+
assert find_piped_pytest_violation(SCRIPT_OPERAND_CARRYING_A_MODULE_FLAG) is None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@pytest.mark.parametrize("each_command", ALL_INTERPRETER_MODULE_RUN_PYTEST_COMMANDS)
|
|
387
|
+
def test_denies_an_interpreter_module_run_of_pytest(each_command: str) -> None:
|
|
388
|
+
"""An interpreter's own ``-m pytest`` still denies, glued or behind a valued option."""
|
|
389
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_denies_a_pipe_carried_only_inside_a_wrapper_command_string() -> None:
|
|
393
|
+
"""The pipe prefilter reads the whole command, so a wrapper's inner pipe still counts."""
|
|
394
|
+
assert find_piped_pytest_violation(WRAPPER_CARRYING_THE_ONLY_PIPE) == CORRECTIVE_MESSAGE
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@pytest.mark.parametrize("each_command", ALL_FLAGGED_WRAPPER_PYTEST_COMMANDS)
|
|
398
|
+
def test_denies_a_pytest_run_behind_a_wrapper_carrying_its_own_flags(each_command: str) -> None:
|
|
399
|
+
"""A wrapper's own flags sit before the program, so the run behind them still counts."""
|
|
400
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@pytest.mark.parametrize("each_command", ALL_CLUSTERED_SHORT_OPTION_PYTEST_COMMANDS)
|
|
404
|
+
def test_denies_a_pytest_run_behind_a_clustered_short_option_wrapper(each_command: str) -> None:
|
|
405
|
+
"""A cluster ending in a value-taking flag takes the token after it, not the program."""
|
|
406
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@pytest.mark.parametrize("each_command", ALL_SHELL_OPTION_VALUE_PYTEST_COMMANDS)
|
|
410
|
+
def test_denies_a_wrapped_pytest_run_behind_a_shell_option_carrying_a_value(
|
|
411
|
+
each_command: str,
|
|
412
|
+
) -> None:
|
|
413
|
+
"""A shell option's value is not the operand, so the string-exec flag behind it counts."""
|
|
414
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@pytest.mark.parametrize("each_command", ALL_CLUSTERED_STRING_EXEC_PYTEST_COMMANDS)
|
|
418
|
+
def test_denies_a_wrapped_pytest_run_behind_a_clustered_string_exec_flag(
|
|
419
|
+
each_command: str,
|
|
420
|
+
) -> None:
|
|
421
|
+
"""A POSIX shell reads the ``-c`` inside ``-euc``, so the string behind it is a command."""
|
|
422
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@pytest.mark.parametrize("each_command", ALL_COVERAGE_RUN_PYTEST_COMMANDS)
|
|
426
|
+
def test_denies_a_pytest_run_behind_a_coverage_run_wrapper(each_command: str) -> None:
|
|
427
|
+
"""``coverage run`` exits with the program's code, so the pytest behind it counts."""
|
|
428
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@pytest.mark.parametrize("each_command", ALL_TOOL_RUNNER_WRAPPER_PYTEST_COMMANDS)
|
|
432
|
+
def test_denies_a_pytest_run_behind_a_tool_runner_wrapper(each_command: str) -> None:
|
|
433
|
+
"""Each tool runner exits with the program's code, so the pytest behind it counts."""
|
|
434
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@pytest.mark.parametrize("each_command", ALL_ALTERNATE_INTERPRETER_PYTEST_COMMANDS)
|
|
438
|
+
def test_denies_a_pytest_module_run_under_an_alternate_interpreter(each_command: str) -> None:
|
|
439
|
+
"""A free-threaded build and PyPy run the module the same way CPython does."""
|
|
440
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def test_allows_a_heredoc_whose_body_line_is_its_terminator_with_surrounding_spaces() -> None:
|
|
444
|
+
"""Bash closes a ``<<`` heredoc on the exact word only, so a spaced lookalike is body text."""
|
|
445
|
+
assert find_piped_pytest_violation(HEREDOC_BODY_LINE_SPACED_LIKE_ITS_TERMINATOR) is None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_denies_a_live_run_below_a_heredoc_a_spaced_lookalike_did_not_close() -> None:
|
|
449
|
+
"""Scanning resumes at the exact terminator, so the run under it is live and denies."""
|
|
450
|
+
assert (
|
|
451
|
+
find_piped_pytest_violation(LIVE_RUN_BELOW_A_SPACED_TERMINATOR_LOOKALIKE)
|
|
452
|
+
== CORRECTIVE_MESSAGE
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def test_denies_a_live_run_below_a_tab_indented_terminator_that_closes_its_heredoc() -> None:
|
|
457
|
+
"""``<<-`` strips leading tabs, so a tab-indented terminator still closes the body."""
|
|
458
|
+
assert (
|
|
459
|
+
find_piped_pytest_violation(LIVE_RUN_BELOW_A_TAB_INDENTED_TERMINATOR) == CORRECTIVE_MESSAGE
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def test_denies_a_piped_pytest_run_placed_below_a_here_string() -> None:
|
|
464
|
+
"""``<<<`` feeds one line rather than opening a body, so the lines under it run."""
|
|
465
|
+
assert find_piped_pytest_violation(HERE_STRING_ABOVE_A_PIPED_PYTEST_RUN) == CORRECTIVE_MESSAGE
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@pytest.mark.parametrize("each_command", ALL_WINDOWS_SHIM_PYTEST_COMMANDS)
|
|
469
|
+
def test_denies_a_windows_shim_spelling_of_a_pytest_run(each_command: str) -> None:
|
|
470
|
+
"""A ``.bat`` or ``.cmd`` shim runs the same suite as the bare spelling."""
|
|
471
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@pytest.mark.parametrize("each_command", ALL_PASS_THROUGH_WRAPPER_PYTEST_COMMANDS)
|
|
475
|
+
def test_denies_a_pytest_run_behind_a_pass_through_wrapper(each_command: str) -> None:
|
|
476
|
+
"""A wrapper exits with the program's code, so the pytest behind it still counts."""
|
|
477
|
+
assert find_piped_pytest_violation(each_command) == CORRECTIVE_MESSAGE
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_denies_a_piped_pytest_run_placed_after_a_heredoc_body() -> None:
|
|
481
|
+
"""Scanning resumes at the terminator, so a live pipe below it still denies."""
|
|
482
|
+
assert find_piped_pytest_violation(COMMAND_AFTER_A_HEREDOC) == CORRECTIVE_MESSAGE
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def test_denies_a_subshell_opened_and_closed_on_different_lines() -> None:
|
|
486
|
+
"""A parenthesis group spanning lines is one command, so the pipe after it denies."""
|
|
487
|
+
assert find_piped_pytest_violation(MULTI_LINE_SUBSHELL_PYTEST_COMMAND) == CORRECTIVE_MESSAGE
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def test_allows_a_subshell_whose_heredoc_body_carries_a_piped_pytest_line() -> None:
|
|
491
|
+
"""Body lines inside a grouped heredoc stay script text, so the group writes only."""
|
|
492
|
+
assert find_piped_pytest_violation(SUBSHELL_WRITING_A_PIPED_PYTEST_HEREDOC) is None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def test_denies_a_subshell_whose_own_line_is_a_comment() -> None:
|
|
496
|
+
"""A comment line inside a group ends at its own newline, so the pipe after it denies."""
|
|
497
|
+
assert find_piped_pytest_violation(SUBSHELL_CARRYING_A_COMMENT_LINE) == CORRECTIVE_MESSAGE
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def test_denies_a_subshell_whose_pytest_line_ends_in_a_comment() -> None:
|
|
501
|
+
"""A trailing comment hides only the rest of its own line, not the lines below it."""
|
|
502
|
+
assert find_piped_pytest_violation(SUBSHELL_CARRYING_A_TRAILING_COMMENT) == CORRECTIVE_MESSAGE
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def test_denies_a_piped_pytest_line_below_a_comment_holding_an_open_paren() -> None:
|
|
506
|
+
"""A parenthesis inside a comment opens no group, so the next line still denies."""
|
|
507
|
+
assert find_piped_pytest_violation(COMMENT_CARRYING_A_STRAY_OPEN_PAREN) == CORRECTIVE_MESSAGE
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def test_denies_a_piped_pytest_line_below_a_comment_naming_a_heredoc_opener() -> None:
|
|
511
|
+
"""A ``<<WORD`` inside a comment opens no heredoc, so the line below stays live."""
|
|
512
|
+
assert find_piped_pytest_violation(COMMENT_NAMING_A_HEREDOC_OPENER) == CORRECTIVE_MESSAGE
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_denies_a_piped_pytest_line_below_a_trailing_comment_naming_a_heredoc_opener() -> None:
|
|
516
|
+
"""A ``<<WORD`` in a line's trailing comment opens no heredoc either."""
|
|
517
|
+
assert (
|
|
518
|
+
find_piped_pytest_violation(TRAILING_COMMENT_NAMING_A_HEREDOC_OPENER) == CORRECTIVE_MESSAGE
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@pytest.mark.parametrize("each_terminator", ALL_LINE_CONTINUATION_TERMINATORS)
|
|
523
|
+
def test_denies_a_continued_pytest_line_for_every_terminator(each_terminator: str) -> None:
|
|
524
|
+
"""A backslash continuation reads as one logical line for each line ending."""
|
|
525
|
+
continued_command = f"python -m pytest tests \\{each_terminator}| tee run.log"
|
|
526
|
+
assert find_piped_pytest_violation(continued_command) == CORRECTIVE_MESSAGE
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def test_quoted_pipe_reads_as_unpiped_here_and_as_a_cut_by_the_shared_splitter() -> None:
|
|
530
|
+
"""One input, both readings: this hook allows it, the shared splitter cuts it.
|
|
531
|
+
|
|
532
|
+
The shared splitter's segments for the input are pinned, so a quote-aware
|
|
533
|
+
``shell_command_segments`` turns this test red — the signal to move this
|
|
534
|
+
module onto the shared helper.
|
|
535
|
+
"""
|
|
536
|
+
assert find_piped_pytest_violation(QUOTED_PIPE_COMMAND) is None
|
|
537
|
+
assert (
|
|
538
|
+
split_into_segments(shlex.split(QUOTED_PIPE_COMMAND))
|
|
539
|
+
== SHARED_SPLITTER_SEGMENTS_FOR_QUOTED_PIPE
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def test_segment_leading_with_pytest_is_recognized() -> None:
|
|
544
|
+
"""A segment whose program is pytest reads as a pytest run."""
|
|
545
|
+
assert segment_runs_pytest(["pytest", "tests", "-q"]) is True
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def test_segment_running_another_python_module_is_not_pytest() -> None:
|
|
549
|
+
"""An interpreter running a different module reads as no pytest run."""
|
|
550
|
+
assert segment_runs_pytest(["python", "-m", "mypy", "."]) is False
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def test_hook_emits_a_deny_payload_for_a_piped_pytest_run() -> None:
|
|
554
|
+
"""The standalone hook writes the deny message for a piped pytest command."""
|
|
555
|
+
completed = _run_process(SCRIPT_PATH, _bash_payload("pytest tests | tee run.log"))
|
|
556
|
+
assert _deny_reason_from_stdout(completed.stdout) == CORRECTIVE_MESSAGE
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def test_deny_message_names_running_pytest_alone_and_the_exit_code() -> None:
|
|
560
|
+
"""The deny message states the fix and why the pipe hides a failure."""
|
|
561
|
+
completed = _run_process(SCRIPT_PATH, _bash_payload("pytest | tee run.log"))
|
|
562
|
+
deny_reason = _deny_reason_from_stdout(completed.stdout)
|
|
563
|
+
assert "run pytest alone" in deny_reason.lower()
|
|
564
|
+
assert "exit code" in deny_reason.lower()
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def test_hook_stays_silent_on_a_bare_pytest_run() -> None:
|
|
568
|
+
"""A pytest command with no pipe produces no decision."""
|
|
569
|
+
completed = _run_process(SCRIPT_PATH, _bash_payload("pytest tests"))
|
|
570
|
+
assert completed.stdout == ""
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def test_ignores_an_unsupported_tool_name() -> None:
|
|
574
|
+
"""A non-Bash tool payload produces no decision."""
|
|
575
|
+
payload = {"tool_name": "Edit", "tool_input": {"command": "pytest | tee run.log"}}
|
|
576
|
+
assert _run_process(SCRIPT_PATH, payload).stdout == ""
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def test_ignores_a_payload_without_a_command() -> None:
|
|
580
|
+
"""A Bash payload carrying no command produces no decision."""
|
|
581
|
+
assert _run_process(SCRIPT_PATH, {"tool_name": "Bash", "tool_input": {}}).stdout == ""
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def test_dispatcher_denies_a_piped_pytest_run() -> None:
|
|
585
|
+
"""The hosted-hook chain surfaces the deny, so the roster carries the segment."""
|
|
586
|
+
completed = _run_process(DISPATCHER_PATH, _bash_payload("pytest tests | tee run.log"))
|
|
587
|
+
assert "exit code" in _deny_reason_from_stdout(completed.stdout).lower()
|