claude-dev-env 1.29.3 → 1.30.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/agents/code-quality-agent.md +279 -24
- package/agents/groq-coder.md +111 -0
- package/commands/plan.md +4 -5
- package/hooks/blocking/code_rules_enforcer.py +775 -8
- package/hooks/blocking/destructive_command_blocker.py +149 -12
- package/hooks/blocking/test_code_rules_enforcer.py +751 -0
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +130 -0
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +134 -0
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +150 -0
- package/hooks/blocking/test_destructive_command_blocker.py +281 -4
- package/hooks/git-hooks/test_config.py +9 -3
- package/hooks/git-hooks/test_gate_utils.py +9 -3
- package/hooks/git-hooks/test_pre_commit.py +9 -3
- package/hooks/git-hooks/test_pre_push.py +9 -3
- package/hooks/validators/run_all_validators.py +76 -3
- package/hooks/validators/test_output_formatter.py +4 -16
- package/hooks/validators/test_run_all_validators.py +22 -0
- package/hooks/validators/test_run_all_validators_integration.py +2 -11
- package/package.json +1 -1
- package/scripts/config/groq_bugteam_config.py +104 -0
- package/scripts/config/test_groq_bugteam_config.py +11 -0
- package/scripts/config/test_spec_implementer_prompt.py +36 -0
- package/scripts/groq_bugteam.README.md +2 -0
- package/scripts/groq_bugteam.py +74 -15
- package/scripts/groq_bugteam_dotenv.py +40 -0
- package/scripts/groq_bugteam_spec.py +226 -0
- package/scripts/test_groq_bugteam.py +143 -5
- package/scripts/test_groq_bugteam_apply_fix_from_spec.py +426 -0
- package/scripts/test_groq_bugteam_dotenv.py +66 -0
- package/scripts/test_groq_bugteam_spec.py +346 -0
- package/skills/bugteam/SKILL.md +4 -0
- package/skills/bugteam/reference/README.md +16 -0
- package/skills/bugteam/test_skill_additions.py +30 -0
- package/skills/monitor-open-prs/SKILL.md +104 -0
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +69 -0
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +149 -0
- package/skills/monitor-open-prs/test_skill_contract.py +43 -0
- package/skills/pr-review-responder/SKILL.md +10 -8
- package/hooks/github-action/pre-push-review.yml +0 -27
- package/hooks/github-action/test_workflow.py +0 -33
- package/skills/pr-review-responder/update_skill.py +0 -297
|
@@ -90,7 +90,7 @@ def test_suppresses_output_on_gh_redirect_deny() -> None:
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def test_asks_on_rm_rf_still_works() -> None:
|
|
93
|
-
payload = _make_bash_payload("rm -rf /
|
|
93
|
+
payload = _make_bash_payload("rm -rf /var/log/myapp")
|
|
94
94
|
result = _run_hook(payload)
|
|
95
95
|
response = json.loads(result.stdout)
|
|
96
96
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
@@ -159,10 +159,287 @@ def test_gh_api_post_comment_is_allowed_when_redirect_env_var_is_unset() -> None
|
|
|
159
159
|
assert result.returncode == 0
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
def
|
|
163
|
-
|
|
162
|
+
def _run_rm_hook(payload: dict) -> subprocess.CompletedProcess[str]:
|
|
163
|
+
child_environment = os.environ.copy()
|
|
164
|
+
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
165
|
+
child_environment.pop("CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW", None)
|
|
166
|
+
return subprocess.run(
|
|
167
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
168
|
+
input=json.dumps(payload),
|
|
169
|
+
text=True,
|
|
170
|
+
capture_output=True,
|
|
171
|
+
check=False,
|
|
172
|
+
env=child_environment,
|
|
173
|
+
)
|
|
164
174
|
|
|
165
|
-
|
|
175
|
+
|
|
176
|
+
def test_rm_rf_asks_when_target_is_non_ephemeral_path() -> None:
|
|
177
|
+
payload = _make_bash_payload("rm -rf /var/log/myapp")
|
|
178
|
+
|
|
179
|
+
result = _run_rm_hook(payload)
|
|
180
|
+
|
|
181
|
+
response = json.loads(result.stdout)
|
|
182
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_rm_rf_allowed_when_target_is_under_tmp_segment() -> None:
|
|
186
|
+
payload = _make_bash_payload("rm -rf /tmp/some_scratch_dir")
|
|
187
|
+
|
|
188
|
+
result = _run_rm_hook(payload)
|
|
189
|
+
|
|
190
|
+
assert result.stdout.strip() == ""
|
|
191
|
+
assert result.returncode == 0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_rm_rf_allowed_when_target_is_under_os_temp_directory() -> None:
|
|
195
|
+
system_temp_subdirectory = Path(tempfile.mkdtemp(prefix="rm_target_"))
|
|
196
|
+
forward_slash_temp_path = str(system_temp_subdirectory).replace("\\", "/")
|
|
197
|
+
payload = _make_bash_payload(f"rm -rf {forward_slash_temp_path}")
|
|
198
|
+
|
|
199
|
+
result = _run_rm_hook(payload)
|
|
200
|
+
|
|
201
|
+
assert result.stdout.strip() == ""
|
|
202
|
+
assert result.returncode == 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_rm_rf_allowed_when_target_is_under_worktrees_segment() -> None:
|
|
206
|
+
payload = _make_bash_payload("rm -rf /Users/me/repo/worktrees/feature_branch/build")
|
|
207
|
+
|
|
208
|
+
result = _run_rm_hook(payload)
|
|
209
|
+
|
|
210
|
+
assert result.stdout.strip() == ""
|
|
211
|
+
assert result.returncode == 0
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_rm_rf_asks_when_target_is_bare_worktrees_directory() -> None:
|
|
215
|
+
payload = _make_bash_payload("rm -rf /Users/me/repo/worktrees")
|
|
216
|
+
|
|
217
|
+
result = _run_rm_hook(payload)
|
|
218
|
+
|
|
219
|
+
response = json.loads(result.stdout)
|
|
220
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_rm_rf_asks_when_rm_includes_option_with_equals_sign() -> None:
|
|
224
|
+
payload = _make_bash_payload("rm -rf --files0-from=/tmp/list /tmp/scratch")
|
|
225
|
+
|
|
226
|
+
result = _run_rm_hook(payload)
|
|
227
|
+
|
|
228
|
+
response = json.loads(result.stdout)
|
|
229
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_rm_rf_allowed_when_both_targets_are_ephemeral() -> None:
|
|
233
|
+
payload = _make_bash_payload("rm -rf /tmp/first_dir /tmp/second_dir")
|
|
234
|
+
|
|
235
|
+
result = _run_rm_hook(payload)
|
|
236
|
+
|
|
237
|
+
assert result.stdout.strip() == ""
|
|
238
|
+
assert result.returncode == 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_rm_rf_asks_when_any_target_is_non_ephemeral() -> None:
|
|
242
|
+
payload = _make_bash_payload("rm -rf /tmp/scratch /etc/passwd")
|
|
243
|
+
|
|
244
|
+
result = _run_rm_hook(payload)
|
|
245
|
+
|
|
246
|
+
response = json.loads(result.stdout)
|
|
247
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_rm_rf_asks_when_double_dash_includes_hyphen_prefixed_non_ephemeral_target() -> None:
|
|
251
|
+
payload = _make_bash_payload("rm -rf -- /tmp/scratch -non_ephemeral")
|
|
252
|
+
|
|
253
|
+
result = _run_rm_hook(payload)
|
|
254
|
+
|
|
255
|
+
response = json.loads(result.stdout)
|
|
256
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_rm_rf_asks_when_command_is_compound_with_ampersand() -> None:
|
|
260
|
+
payload = _make_bash_payload("rm -rf /tmp/reply && gh pr checks 19")
|
|
261
|
+
|
|
262
|
+
result = _run_rm_hook(payload)
|
|
263
|
+
|
|
264
|
+
response = json.loads(result.stdout)
|
|
265
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_rm_rf_asks_when_target_is_bare_tmp_root() -> None:
|
|
269
|
+
payload = _make_bash_payload("rm -rf /tmp")
|
|
270
|
+
|
|
271
|
+
result = _run_rm_hook(payload)
|
|
272
|
+
|
|
273
|
+
response = json.loads(result.stdout)
|
|
274
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_rm_rf_asks_when_target_is_double_slash_tmp_root() -> None:
|
|
278
|
+
payload = _make_bash_payload("rm -rf //tmp")
|
|
279
|
+
|
|
280
|
+
result = _run_rm_hook(payload)
|
|
281
|
+
|
|
282
|
+
response = json.loads(result.stdout)
|
|
283
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_rm_rf_asks_when_target_is_bare_os_temp_root() -> None:
|
|
287
|
+
payload = _make_bash_payload(f"rm -rf {tempfile.gettempdir()}")
|
|
288
|
+
|
|
289
|
+
result = _run_rm_hook(payload)
|
|
290
|
+
|
|
291
|
+
response = json.loads(result.stdout)
|
|
292
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_rm_rf_asks_when_ephemeral_auto_allow_disabled_via_env_var() -> None:
|
|
296
|
+
payload = _make_bash_payload("rm -rf /tmp/scratch")
|
|
297
|
+
|
|
298
|
+
child_environment = os.environ.copy()
|
|
299
|
+
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
300
|
+
child_environment["CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW"] = "1"
|
|
301
|
+
result = subprocess.run(
|
|
302
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
303
|
+
input=json.dumps(payload),
|
|
304
|
+
text=True,
|
|
305
|
+
capture_output=True,
|
|
306
|
+
check=False,
|
|
307
|
+
env=child_environment,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
response = json.loads(result.stdout)
|
|
311
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_rm_recursive_force_long_flags_allowed_under_tmp() -> None:
|
|
315
|
+
payload = _make_bash_payload("rm --recursive --force /tmp/long_flag_scratch")
|
|
316
|
+
|
|
317
|
+
result = _run_rm_hook(payload)
|
|
318
|
+
|
|
319
|
+
assert result.stdout.strip() == ""
|
|
320
|
+
assert result.returncode == 0
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_rm_rf_asks_when_quoted_dotdot_traverses_out_of_ephemeral_root() -> None:
|
|
324
|
+
payload = _make_bash_payload('rm -rf /tmp/".."/etc')
|
|
325
|
+
|
|
326
|
+
result = _run_rm_hook(payload)
|
|
327
|
+
|
|
328
|
+
response = json.loads(result.stdout)
|
|
329
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_rm_rf_allowed_when_quoted_path_is_legitimate_ephemeral() -> None:
|
|
333
|
+
payload = _make_bash_payload('rm -rf "/tmp/some scratch dir"')
|
|
334
|
+
|
|
335
|
+
result = _run_rm_hook(payload)
|
|
336
|
+
|
|
337
|
+
assert result.stdout.strip() == ""
|
|
338
|
+
assert result.returncode == 0
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_rm_rf_asks_when_single_quoted_dotdot_traverses_out_of_ephemeral() -> None:
|
|
342
|
+
payload = _make_bash_payload("rm -rf /tmp/'..'/etc")
|
|
343
|
+
|
|
344
|
+
result = _run_rm_hook(payload)
|
|
345
|
+
|
|
346
|
+
response = json.loads(result.stdout)
|
|
347
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_rm_rf_asks_when_target_is_glob_wildcard_under_tmp() -> None:
|
|
351
|
+
payload = _make_bash_payload("rm -rf /tmp/*")
|
|
352
|
+
|
|
353
|
+
result = _run_rm_hook(payload)
|
|
354
|
+
|
|
355
|
+
response = json.loads(result.stdout)
|
|
356
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_rm_rf_asks_when_target_is_question_mark_glob_under_tmp() -> None:
|
|
360
|
+
payload = _make_bash_payload("rm -rf /tmp/?")
|
|
361
|
+
|
|
362
|
+
result = _run_rm_hook(payload)
|
|
363
|
+
|
|
364
|
+
response = json.loads(result.stdout)
|
|
365
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_rm_rf_asks_when_target_is_bracket_glob_under_tmp() -> None:
|
|
369
|
+
payload = _make_bash_payload("rm -rf /tmp/[abc]")
|
|
370
|
+
|
|
371
|
+
result = _run_rm_hook(payload)
|
|
372
|
+
|
|
373
|
+
response = json.loads(result.stdout)
|
|
374
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def test_rm_rf_asks_when_target_is_worktrees_glob() -> None:
|
|
378
|
+
payload = _make_bash_payload("rm -rf /worktrees/*")
|
|
379
|
+
|
|
380
|
+
result = _run_rm_hook(payload)
|
|
381
|
+
|
|
382
|
+
response = json.loads(result.stdout)
|
|
383
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def test_rm_rf_asks_when_target_is_os_temp_root_glob() -> None:
|
|
387
|
+
system_temporary_root = tempfile.gettempdir()
|
|
388
|
+
forward_slash_temp_root = system_temporary_root.replace("\\", "/")
|
|
389
|
+
payload = _make_bash_payload(f"rm -rf {forward_slash_temp_root}/*")
|
|
390
|
+
|
|
391
|
+
result = _run_rm_hook(payload)
|
|
392
|
+
|
|
393
|
+
response = json.loads(result.stdout)
|
|
394
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_rm_rf_allowed_when_unquoted_windows_backslash_target_is_ephemeral() -> None:
|
|
398
|
+
system_temporary_root = tempfile.gettempdir()
|
|
399
|
+
windows_style_target = system_temporary_root.replace("/", "\\") + "\\scratch"
|
|
400
|
+
payload = _make_bash_payload(f"rm -rf {windows_style_target}")
|
|
401
|
+
|
|
402
|
+
result = _run_rm_hook(payload)
|
|
403
|
+
|
|
404
|
+
assert result.stdout.strip() == ""
|
|
405
|
+
assert result.returncode == 0
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def test_rm_rf_allowed_when_unquoted_windows_backslash_target_contains_worktrees_segment() -> None:
|
|
409
|
+
payload = _make_bash_payload(
|
|
410
|
+
r"rm -rf C:\Users\developer\project\worktrees\feature_branch\build"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
result = _run_rm_hook(payload)
|
|
414
|
+
|
|
415
|
+
assert result.stdout.strip() == ""
|
|
416
|
+
assert result.returncode == 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_rm_rf_allowed_when_finding_example_windows_backslash_ephemeral_target() -> None:
|
|
420
|
+
payload = _make_bash_payload(
|
|
421
|
+
r"rm -rf C:\Users\jon\AppData\Local\Temp\scratch"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
result = _run_rm_hook(payload)
|
|
425
|
+
|
|
426
|
+
assert result.stdout.strip() == ""
|
|
427
|
+
assert result.returncode == 0
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_rm_rf_asks_when_target_is_literal_tmp_star_finding_example() -> None:
|
|
431
|
+
payload = _make_bash_payload("rm -rf /tmp/*")
|
|
432
|
+
|
|
433
|
+
result = _run_rm_hook(payload)
|
|
434
|
+
|
|
435
|
+
response = json.loads(result.stdout)
|
|
436
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_rm_rf_asks_when_target_basename_is_wildcard_with_prefix_under_tmp() -> None:
|
|
440
|
+
payload = _make_bash_payload("rm -rf /tmp/foo*")
|
|
441
|
+
|
|
442
|
+
result = _run_rm_hook(payload)
|
|
166
443
|
|
|
167
444
|
response = json.loads(result.stdout)
|
|
168
445
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import importlib
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
sys.
|
|
9
|
+
_git_hooks_directory_string = str(SCRIPT_DIRECTORY)
|
|
10
|
+
while _git_hooks_directory_string in sys.path:
|
|
11
|
+
sys.path.remove(_git_hooks_directory_string)
|
|
12
|
+
sys.path.insert(0, _git_hooks_directory_string)
|
|
13
|
+
for each_module_name in list(sys.modules):
|
|
14
|
+
if each_module_name == "config" or each_module_name.startswith("config."):
|
|
15
|
+
del sys.modules[each_module_name]
|
|
16
|
+
importlib.invalidate_caches()
|
|
11
17
|
|
|
12
18
|
import config
|
|
13
19
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import importlib
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -7,9 +8,14 @@ import pytest
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
sys.
|
|
11
|
+
_git_hooks_directory_string = str(SCRIPT_DIRECTORY)
|
|
12
|
+
while _git_hooks_directory_string in sys.path:
|
|
13
|
+
sys.path.remove(_git_hooks_directory_string)
|
|
14
|
+
sys.path.insert(0, _git_hooks_directory_string)
|
|
15
|
+
for each_module_name in list(sys.modules):
|
|
16
|
+
if each_module_name == "config" or each_module_name.startswith("config."):
|
|
17
|
+
del sys.modules[each_module_name]
|
|
18
|
+
importlib.invalidate_caches()
|
|
13
19
|
|
|
14
20
|
import gate_utils
|
|
15
21
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import importlib
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -7,9 +8,14 @@ import pytest
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
sys.
|
|
11
|
+
_git_hooks_directory_string = str(SCRIPT_DIRECTORY)
|
|
12
|
+
while _git_hooks_directory_string in sys.path:
|
|
13
|
+
sys.path.remove(_git_hooks_directory_string)
|
|
14
|
+
sys.path.insert(0, _git_hooks_directory_string)
|
|
15
|
+
for each_module_name in list(sys.modules):
|
|
16
|
+
if each_module_name == "config" or each_module_name.startswith("config."):
|
|
17
|
+
del sys.modules[each_module_name]
|
|
18
|
+
importlib.invalidate_caches()
|
|
13
19
|
|
|
14
20
|
import pre_commit
|
|
15
21
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import importlib
|
|
3
4
|
import io
|
|
4
5
|
import sys
|
|
5
6
|
from pathlib import Path
|
|
@@ -8,9 +9,14 @@ import pytest
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
sys.
|
|
12
|
+
_git_hooks_directory_string = str(SCRIPT_DIRECTORY)
|
|
13
|
+
while _git_hooks_directory_string in sys.path:
|
|
14
|
+
sys.path.remove(_git_hooks_directory_string)
|
|
15
|
+
sys.path.insert(0, _git_hooks_directory_string)
|
|
16
|
+
for each_module_name in list(sys.modules):
|
|
17
|
+
if each_module_name == "config" or each_module_name.startswith("config."):
|
|
18
|
+
del sys.modules[each_module_name]
|
|
19
|
+
importlib.invalidate_caches()
|
|
14
20
|
|
|
15
21
|
import pre_push
|
|
16
22
|
import config
|
|
@@ -6,8 +6,10 @@ Exit code 0 = all checks pass, 1 = violations found.
|
|
|
6
6
|
# pragma: no-tdd-gate
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
|
+
import os
|
|
9
10
|
import subprocess
|
|
10
11
|
import sys
|
|
12
|
+
import tempfile
|
|
11
13
|
import time
|
|
12
14
|
from dataclasses import dataclass
|
|
13
15
|
from datetime import datetime
|
|
@@ -26,18 +28,89 @@ hooks_dir = VALIDATORS_DIR.parent
|
|
|
26
28
|
package_name = VALIDATORS_DIR.name
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
def _windows_non_unc_working_directory_string(
|
|
32
|
+
candidate_directory_strings: list[str | None],
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Return the first candidate cwd that is not a UNC path (Windows only)."""
|
|
35
|
+
for each_candidate in candidate_directory_strings:
|
|
36
|
+
if each_candidate is None:
|
|
37
|
+
continue
|
|
38
|
+
expanded_candidate = str(Path(each_candidate).expanduser())
|
|
39
|
+
if expanded_candidate.startswith("\\\\"):
|
|
40
|
+
continue
|
|
41
|
+
return expanded_candidate
|
|
42
|
+
current_working_directory = os.getcwd()
|
|
43
|
+
expanded_current_working_directory = str(Path(current_working_directory).expanduser())
|
|
44
|
+
if not expanded_current_working_directory.startswith("\\\\"):
|
|
45
|
+
return expanded_current_working_directory
|
|
46
|
+
raise RuntimeError(
|
|
47
|
+
"Cannot find a non-UNC working directory for hook validator subprocesses."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _hooks_subprocess_working_directory_and_environment() -> tuple[str, dict[str, str]]:
|
|
52
|
+
"""Return cwd and env for validator subprocesses.
|
|
53
|
+
|
|
54
|
+
On Windows, ``CreateProcess`` rejects some UNC working directories (invalid
|
|
55
|
+
directory name). When the hooks tree resolves to UNC, use a local temp cwd
|
|
56
|
+
and put the hooks directory on ``PYTHONPATH`` so ``python -m validators.*``
|
|
57
|
+
still resolves.
|
|
58
|
+
"""
|
|
59
|
+
hooks_directory_string = str(hooks_dir.resolve())
|
|
60
|
+
environment = os.environ.copy()
|
|
61
|
+
previous_pythonpath = environment.get("PYTHONPATH", "")
|
|
62
|
+
environment["PYTHONPATH"] = (
|
|
63
|
+
hooks_directory_string
|
|
64
|
+
+ (os.pathsep + previous_pythonpath if previous_pythonpath else "")
|
|
65
|
+
)
|
|
66
|
+
working_directory_string = hooks_directory_string
|
|
67
|
+
if sys.platform == "win32" and working_directory_string.startswith("\\\\"):
|
|
68
|
+
windows_temp_fallback_directory = str(Path(r"C:\Windows\Temp"))
|
|
69
|
+
working_directory_string = _windows_non_unc_working_directory_string(
|
|
70
|
+
[
|
|
71
|
+
os.environ.get("TEMP"),
|
|
72
|
+
os.environ.get("TMP"),
|
|
73
|
+
tempfile.gettempdir(),
|
|
74
|
+
windows_temp_fallback_directory,
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
return working_directory_string, environment
|
|
78
|
+
|
|
79
|
+
|
|
29
80
|
def invoke_validator_module(module_stem: str, forwarded_file_paths: List[str]) -> subprocess.CompletedProcess[str]: # pragma: no-tdd-gate
|
|
30
81
|
"""Run a sibling validator as ``python -m validators.<module_stem>``.
|
|
31
82
|
|
|
32
|
-
The subprocess
|
|
33
|
-
``
|
|
83
|
+
The subprocess uses the hooks tree on ``PYTHONPATH`` (and normally ``cwd``
|
|
84
|
+
there). On Windows, if that path is UNC, ``cwd`` falls back to a local temp
|
|
85
|
+
directory so ``CreateProcess`` succeeds.
|
|
34
86
|
"""
|
|
35
87
|
qualified_module = ".".join([package_name, module_stem])
|
|
88
|
+
working_directory_string, environment = (
|
|
89
|
+
_hooks_subprocess_working_directory_and_environment()
|
|
90
|
+
)
|
|
36
91
|
return subprocess.run(
|
|
37
92
|
[sys.executable, "-m", qualified_module, *forwarded_file_paths],
|
|
38
93
|
capture_output=True,
|
|
39
94
|
text=True,
|
|
40
|
-
cwd=
|
|
95
|
+
cwd=working_directory_string,
|
|
96
|
+
env=environment,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_validators_entrypoint_subprocess(
|
|
101
|
+
extra_arguments: List[str],
|
|
102
|
+
) -> subprocess.CompletedProcess[str]:
|
|
103
|
+
"""Run ``python -m validators.run_all_validators`` with a Windows-safe cwd."""
|
|
104
|
+
working_directory_string, environment = (
|
|
105
|
+
_hooks_subprocess_working_directory_and_environment()
|
|
106
|
+
)
|
|
107
|
+
entry_module = f"{package_name}.run_all_validators"
|
|
108
|
+
return subprocess.run(
|
|
109
|
+
[sys.executable, "-m", entry_module, *extra_arguments],
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
cwd=working_directory_string,
|
|
113
|
+
env=environment,
|
|
41
114
|
)
|
|
42
115
|
|
|
43
116
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Tests for output formatting."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import sys
|
|
3
|
+
import json
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
7
6
|
|
|
@@ -15,6 +14,7 @@ from .output_formatter import (
|
|
|
15
14
|
ViolationDict,
|
|
16
15
|
ValidatorResultDict,
|
|
17
16
|
)
|
|
17
|
+
from .run_all_validators import run_validators_entrypoint_subprocess
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class TestColorize:
|
|
@@ -93,21 +93,9 @@ class TestOutputFormatter:
|
|
|
93
93
|
|
|
94
94
|
class TestJsonFlag:
|
|
95
95
|
def test_json_flag_produces_valid_json(self) -> None:
|
|
96
|
-
|
|
97
|
-
import subprocess
|
|
96
|
+
completed_validation_run = run_validators_entrypoint_subprocess(["--json"])
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
hooks_directory = os.path.normpath(
|
|
101
|
-
os.path.join(validators_directory, os.pardir)
|
|
102
|
-
)
|
|
103
|
-
result = subprocess.run(
|
|
104
|
-
[sys.executable, "-m", "validators.run_all_validators", "--json"],
|
|
105
|
-
capture_output=True,
|
|
106
|
-
text=True,
|
|
107
|
-
cwd=hooks_directory,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
output = result.stdout.strip()
|
|
98
|
+
output = completed_validation_run.stdout.strip()
|
|
111
99
|
parsed = json.loads(output)
|
|
112
100
|
assert "results" in parsed
|
|
113
101
|
assert isinstance(parsed["results"], list)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Tests for run_all_validators.py."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
5
|
+
import tempfile
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from unittest.mock import MagicMock, patch
|
|
6
8
|
|
|
@@ -8,6 +10,7 @@ import pytest
|
|
|
8
10
|
|
|
9
11
|
from .run_all_validators import (
|
|
10
12
|
ValidatorResult,
|
|
13
|
+
_hooks_subprocess_working_directory_and_environment,
|
|
11
14
|
add_timing,
|
|
12
15
|
build_json_output,
|
|
13
16
|
create_timing_metrics,
|
|
@@ -258,3 +261,22 @@ class TestVersionHeader:
|
|
|
258
261
|
assert "version" in json_output
|
|
259
262
|
assert "timestamp" in json_output
|
|
260
263
|
assert isinstance(json_output["version"], str)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class TestHooksSubprocessWorkingDirectory:
|
|
267
|
+
def test_unc_path_fallback_on_windows_uses_tempdir_when_temp_unset(self) -> None:
|
|
268
|
+
mock_hooks_directory = MagicMock()
|
|
269
|
+
mock_hooks_directory.resolve.return_value = Path("\\\\server\\share\\hooks")
|
|
270
|
+
environment_without_temp = {
|
|
271
|
+
each_key: each_value
|
|
272
|
+
for each_key, each_value in os.environ.items()
|
|
273
|
+
if each_key not in ("TEMP", "TMP")
|
|
274
|
+
}
|
|
275
|
+
with patch("validators.run_all_validators.hooks_dir", mock_hooks_directory), patch(
|
|
276
|
+
"validators.run_all_validators.sys.platform", "win32"
|
|
277
|
+
), patch("validators.run_all_validators.os.environ", environment_without_temp):
|
|
278
|
+
working_directory_string, _environment = (
|
|
279
|
+
_hooks_subprocess_working_directory_and_environment()
|
|
280
|
+
)
|
|
281
|
+
assert working_directory_string == tempfile.gettempdir()
|
|
282
|
+
assert not working_directory_string.startswith("\\\\")
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
"""Integration test for new validators in run_all_validators.py"""
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
HOOKS_DIR = VALIDATORS_DIR.parent
|
|
9
|
-
PACKAGE_MODULE = f"{VALIDATORS_DIR.name}.run_all_validators"
|
|
5
|
+
from .run_all_validators import run_validators_entrypoint_subprocess
|
|
10
6
|
|
|
11
7
|
|
|
12
8
|
def run_validators_help() -> subprocess.CompletedProcess[str]:
|
|
13
|
-
return
|
|
14
|
-
[sys.executable, "-m", PACKAGE_MODULE, "--help"],
|
|
15
|
-
capture_output=True,
|
|
16
|
-
text=True,
|
|
17
|
-
cwd=str(HOOKS_DIR),
|
|
18
|
-
)
|
|
9
|
+
return run_validators_entrypoint_subprocess(["--help"])
|
|
19
10
|
|
|
20
11
|
|
|
21
12
|
class TestNewValidatorsIntegration:
|