claude-dev-env 1.30.1 → 1.31.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/clean-coder.md +275 -111
- package/agents/code-quality-agent.md +196 -209
- package/bin/install.mjs +81 -0
- package/bin/install.test.mjs +158 -0
- package/bin/install_mypy_ini.mjs +51 -0
- package/bin/install_mypy_ini.test.mjs +121 -0
- package/commands/hook-log-extract.md +70 -0
- package/commands/hook-log-init.md +76 -0
- package/hooks/blocking/code_rules_enforcer.py +5 -3
- package/hooks/blocking/destructive_command_blocker.py +187 -0
- package/hooks/blocking/question_to_user_enforcer.py +140 -0
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +39 -0
- package/hooks/blocking/test_destructive_command_blocker.py +397 -0
- package/hooks/blocking/test_question_to_user_enforcer.py +163 -0
- package/hooks/config/hook_log_extractor_constants.py +221 -0
- package/hooks/config/messages.py +3 -0
- package/hooks/config/test_hook_log_extractor_constants.py +96 -0
- package/hooks/config/test_messages.py +5 -0
- package/hooks/diagnostic/hook_log_extractor.py +907 -0
- package/hooks/diagnostic/hook_log_init.py +202 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +84 -0
- package/hooks/diagnostic/migrations/2026-04-25-drop-themes-hook-events.sql +3 -0
- package/hooks/diagnostic/migrations/README.md +77 -0
- package/hooks/diagnostic/queries/block_details_for_hook.sql +26 -0
- package/hooks/diagnostic/queries/blocks_by_category.sql +10 -0
- package/hooks/diagnostic/queries/blocks_by_tool.sql +9 -0
- package/hooks/diagnostic/queries/blocks_last_7_days.sql +11 -0
- package/hooks/diagnostic/queries/top_blockers_last_24_hours.sql +12 -0
- package/hooks/diagnostic/queries/top_blockers_overall.sql +12 -0
- package/hooks/diagnostic/requirements-hook-logs-dev.txt +2 -0
- package/hooks/diagnostic/requirements-hook-logs.txt +1 -0
- package/hooks/diagnostic/schema.sql +51 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +1531 -0
- package/hooks/diagnostic/test_hook_log_init.py +227 -0
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +98 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +44 -0
- package/scripts/config/test_spec_implementer_prompt.py +0 -4
- package/scripts/test_groq_bugteam_spec.py +0 -8
|
@@ -265,6 +265,403 @@ def test_rm_rf_asks_when_command_is_compound_with_ampersand() -> None:
|
|
|
265
265
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
266
266
|
|
|
267
267
|
|
|
268
|
+
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_double_quoted() -> None:
|
|
269
|
+
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf .bugteam-tmp')
|
|
270
|
+
|
|
271
|
+
result = _run_rm_hook(payload)
|
|
272
|
+
|
|
273
|
+
assert result.stdout.strip() == ""
|
|
274
|
+
assert result.returncode == 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_single_quoted() -> None:
|
|
278
|
+
payload = _make_bash_payload("cd '/tmp/bugteam_scratch' && rm -rf .bugteam-tmp")
|
|
279
|
+
|
|
280
|
+
result = _run_rm_hook(payload)
|
|
281
|
+
|
|
282
|
+
assert result.stdout.strip() == ""
|
|
283
|
+
assert result.returncode == 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_unquoted() -> None:
|
|
287
|
+
payload = _make_bash_payload("cd /tmp/bugteam_scratch && rm -rf .bugteam-tmp")
|
|
288
|
+
|
|
289
|
+
result = _run_rm_hook(payload)
|
|
290
|
+
|
|
291
|
+
assert result.stdout.strip() == ""
|
|
292
|
+
assert result.returncode == 0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_rm_rf_allowed_when_leading_cd_into_windows_temp_worktree_subdirectory() -> None:
|
|
296
|
+
windows_style_temp_worktree = (
|
|
297
|
+
r"C:\Users\jon\AppData\Local\Temp\bugteam-pr-58-20260424071040\pr-58\worktree"
|
|
298
|
+
)
|
|
299
|
+
payload = _make_bash_payload(
|
|
300
|
+
f'cd "{windows_style_temp_worktree}" && rm -rf .bugteam-tmp'
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
result = _run_rm_hook(payload)
|
|
304
|
+
|
|
305
|
+
assert result.stdout.strip() == ""
|
|
306
|
+
assert result.returncode == 0
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_with_extra_compound_after_rm() -> None:
|
|
310
|
+
payload = _make_bash_payload(
|
|
311
|
+
'cd "/tmp/bugteam_scratch" && rm -rf .bugteam-tmp && gh pr checks 19'
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
result = _run_rm_hook(payload)
|
|
315
|
+
|
|
316
|
+
assert result.stdout.strip() == ""
|
|
317
|
+
assert result.returncode == 0
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_rm_rf_asks_when_leading_cd_into_ephemeral_with_wildcard_target() -> None:
|
|
321
|
+
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf *')
|
|
322
|
+
|
|
323
|
+
result = _run_rm_hook(payload)
|
|
324
|
+
|
|
325
|
+
response = json.loads(result.stdout)
|
|
326
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_rm_rf_asks_when_cwd_ephemeral_and_relative_target_escapes_via_dotdot() -> None:
|
|
330
|
+
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf ../../etc')
|
|
331
|
+
|
|
332
|
+
result = _run_rm_hook(payload)
|
|
333
|
+
|
|
334
|
+
response = json.loads(result.stdout)
|
|
335
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_rm_rf_asks_when_rm_uses_files0_from_long_option_with_equals() -> None:
|
|
339
|
+
payload = _make_bash_payload(
|
|
340
|
+
'cd "/tmp/bugteam_scratch" && rm -rf --files0-from=/etc/passwd'
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
result = _run_rm_hook(payload)
|
|
344
|
+
|
|
345
|
+
response = json.loads(result.stdout)
|
|
346
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_rm_rf_asks_when_rm_uses_unknown_long_option() -> None:
|
|
350
|
+
payload = _make_bash_payload(
|
|
351
|
+
'cd "/tmp/bugteam_scratch" && rm -rf --nuke /tmp/bugteam_scratch/stuff'
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
result = _run_rm_hook(payload)
|
|
355
|
+
|
|
356
|
+
response = json.loads(result.stdout)
|
|
357
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_rm_rf_asks_when_rm_target_uses_windows_backslash_absolute_path_unquoted() -> None:
|
|
361
|
+
payload = _make_bash_payload(
|
|
362
|
+
r'cd "/tmp/bugteam_scratch" && rm -rf C:\sensitive\path'
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
result = _run_rm_hook(payload)
|
|
366
|
+
|
|
367
|
+
response = json.loads(result.stdout)
|
|
368
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_rm_rf_asks_when_relative_target_without_declared_cwd_fails_closed() -> None:
|
|
372
|
+
payload_with_no_cwd = {
|
|
373
|
+
"tool_name": "Bash",
|
|
374
|
+
"tool_input": {"command": "rm -rf relative/path"},
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
result = _run_rm_hook(payload_with_no_cwd)
|
|
378
|
+
|
|
379
|
+
response = json.loads(result.stdout)
|
|
380
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def test_rm_rf_allowed_when_cwd_ephemeral_and_relative_target_stays_within() -> None:
|
|
384
|
+
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf ./build')
|
|
385
|
+
|
|
386
|
+
result = _run_rm_hook(payload)
|
|
387
|
+
|
|
388
|
+
assert result.stdout.strip() == ""
|
|
389
|
+
assert result.returncode == 0
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_rm_rf_asks_when_tool_input_cwd_is_ephemeral_but_rm_target_is_absolute_non_ephemeral() -> None:
|
|
393
|
+
payload_with_tool_input_cwd = {
|
|
394
|
+
"tool_name": "Bash",
|
|
395
|
+
"tool_input": {
|
|
396
|
+
"command": "rm -rf /var/log/myapp",
|
|
397
|
+
"cwd": "/tmp/bugteam_scratch",
|
|
398
|
+
},
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
402
|
+
|
|
403
|
+
response = json.loads(result.stdout)
|
|
404
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def test_git_push_force_asks_when_leading_cd_into_ephemeral_subdirectory() -> None:
|
|
408
|
+
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && git push --force')
|
|
409
|
+
|
|
410
|
+
result = _run_rm_hook(payload)
|
|
411
|
+
|
|
412
|
+
response = json.loads(result.stdout)
|
|
413
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
414
|
+
assert "git push --force" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_git_clean_force_recursive_asks_when_leading_cd_into_ephemeral_subdirectory() -> None:
|
|
418
|
+
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && git clean -fd')
|
|
419
|
+
|
|
420
|
+
result = _run_rm_hook(payload)
|
|
421
|
+
|
|
422
|
+
response = json.loads(result.stdout)
|
|
423
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
424
|
+
assert "git clean -fd" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_rm_rf_plus_git_push_force_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
428
|
+
payload = _make_bash_payload(
|
|
429
|
+
'cd "/tmp/bugteam_scratch" && rm -rf cache && git push --force'
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
result = _run_rm_hook(payload)
|
|
433
|
+
|
|
434
|
+
response = json.loads(result.stdout)
|
|
435
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_rm_rf_plus_git_clean_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
439
|
+
payload = _make_bash_payload(
|
|
440
|
+
'cd "/tmp/bugteam_scratch" && rm -rf cache && git clean -fd'
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
result = _run_rm_hook(payload)
|
|
444
|
+
|
|
445
|
+
response = json.loads(result.stdout)
|
|
446
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def test_rm_rf_plus_mkfs_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
450
|
+
payload = _make_bash_payload(
|
|
451
|
+
'cd "/tmp/bugteam_scratch" && rm -rf cache && mkfs.ext4 /dev/sda1'
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
result = _run_rm_hook(payload)
|
|
455
|
+
|
|
456
|
+
response = json.loads(result.stdout)
|
|
457
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def test_rm_rf_plus_drop_table_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
461
|
+
payload = _make_bash_payload(
|
|
462
|
+
'cd "/tmp/bugteam_scratch" && rm -rf cache && psql -c "DROP TABLE users"'
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
result = _run_rm_hook(payload)
|
|
466
|
+
|
|
467
|
+
response = json.loads(result.stdout)
|
|
468
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def test_rm_rf_asks_when_leading_cd_into_ephemeral_but_rm_target_is_bare_tmp_root() -> None:
|
|
472
|
+
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf /tmp')
|
|
473
|
+
|
|
474
|
+
result = _run_rm_hook(payload)
|
|
475
|
+
|
|
476
|
+
response = json.loads(result.stdout)
|
|
477
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_rm_rf_asks_when_leading_cd_into_ephemeral_but_rm_target_is_bare_worktrees_root() -> None:
|
|
481
|
+
payload = _make_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf /worktrees')
|
|
482
|
+
|
|
483
|
+
result = _run_rm_hook(payload)
|
|
484
|
+
|
|
485
|
+
response = json.loads(result.stdout)
|
|
486
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_rm_rf_asks_when_command_fails_shlex_parse_with_unbalanced_quotes() -> None:
|
|
490
|
+
payload_with_tool_input_cwd = {
|
|
491
|
+
"tool_name": "Bash",
|
|
492
|
+
"tool_input": {
|
|
493
|
+
"command": 'rm -rf "unclosed_quote',
|
|
494
|
+
"cwd": "/tmp/bugteam_scratch",
|
|
495
|
+
},
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
499
|
+
|
|
500
|
+
response = json.loads(result.stdout)
|
|
501
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def test_rm_rf_asks_when_leading_cd_target_is_non_ephemeral_directory() -> None:
|
|
505
|
+
payload = _make_bash_payload('cd "/etc" && rm -rf scratch')
|
|
506
|
+
|
|
507
|
+
result = _run_rm_hook(payload)
|
|
508
|
+
|
|
509
|
+
response = json.loads(result.stdout)
|
|
510
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def test_rm_rf_asks_when_leading_cd_target_is_bare_ephemeral_root() -> None:
|
|
514
|
+
payload = _make_bash_payload('cd "/tmp" && rm -rf scratch')
|
|
515
|
+
|
|
516
|
+
result = _run_rm_hook(payload)
|
|
517
|
+
|
|
518
|
+
response = json.loads(result.stdout)
|
|
519
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def test_rm_rf_allowed_when_leading_cd_target_is_git_worktrees_directory() -> None:
|
|
523
|
+
payload = _make_bash_payload('cd "/Users/me/repo/worktrees" && rm -rf feature')
|
|
524
|
+
|
|
525
|
+
result = _run_rm_hook(payload)
|
|
526
|
+
|
|
527
|
+
assert result.stdout.strip() == ""
|
|
528
|
+
assert result.returncode == 0
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_rm_rf_asks_when_leading_cd_target_is_relative_path() -> None:
|
|
532
|
+
payload = _make_bash_payload('cd "./scratch" && rm -rf inner')
|
|
533
|
+
|
|
534
|
+
result = _run_rm_hook(payload)
|
|
535
|
+
|
|
536
|
+
response = json.loads(result.stdout)
|
|
537
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def test_rm_rf_allowed_for_chat_observed_bugteam_backslash_worktree_scratch_cleanup() -> None:
|
|
541
|
+
payload = _make_bash_payload(
|
|
542
|
+
r'cd "C:\Users\jon\AppData\Local\Temp\bugteam-pr-58-20260424071040\pr-58\worktree" && rm -rf .bugteam-tmp'
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
result = _run_rm_hook(payload)
|
|
546
|
+
|
|
547
|
+
assert result.stdout.strip() == ""
|
|
548
|
+
assert result.returncode == 0
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def test_rm_rf_allowed_for_chat_observed_bugteam_forward_slash_worktree_scratch_cleanup() -> None:
|
|
552
|
+
payload = _make_bash_payload(
|
|
553
|
+
'cd "C:/Users/jon/AppData/Local/Temp/bugteam-pr-58-20260424071040/pr-58/worktree" && rm -rf .bugteam-tmp'
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
result = _run_rm_hook(payload)
|
|
557
|
+
|
|
558
|
+
assert result.stdout.strip() == ""
|
|
559
|
+
assert result.returncode == 0
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def test_rm_rf_allowed_for_chat_observed_bugfix_reply_scratch_file_cleanup() -> None:
|
|
563
|
+
payload = _make_bash_payload(
|
|
564
|
+
'cd "C:/Users/jon/AppData/Local/Temp/bugteam-pr-58-20260424071040/pr-58/worktree" && rm -rf tmp_reply_loop1-1.md'
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
result = _run_rm_hook(payload)
|
|
568
|
+
|
|
569
|
+
assert result.stdout.strip() == ""
|
|
570
|
+
assert result.returncode == 0
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def test_rm_rf_allowed_for_chat_observed_bugfind_multiple_scratch_files_cleanup() -> None:
|
|
574
|
+
payload = _make_bash_payload(
|
|
575
|
+
'cd "C:/Users/jon/AppData/Local/Temp/bugteam-pr-58-20260424071040/pr-256/worktree" && rm -rf tmp_review_body.md tmp_finding_1.md'
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
result = _run_rm_hook(payload)
|
|
579
|
+
|
|
580
|
+
assert result.stdout.strip() == ""
|
|
581
|
+
assert result.returncode == 0
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def test_rm_rf_allowed_via_tool_input_cwd_pointing_at_chat_observed_bugteam_worktree() -> None:
|
|
585
|
+
payload_with_tool_input_cwd = {
|
|
586
|
+
"tool_name": "Bash",
|
|
587
|
+
"tool_input": {
|
|
588
|
+
"command": "rm -rf .bugteam-tmp",
|
|
589
|
+
"cwd": "C:/Users/jon/AppData/Local/Temp/bugteam-pr-58-20260424071040/pr-58/worktree",
|
|
590
|
+
},
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
594
|
+
|
|
595
|
+
assert result.stdout.strip() == ""
|
|
596
|
+
assert result.returncode == 0
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_rm_rf_allowed_for_chat_observed_absolute_path_in_bugteam_windows_worktree_scratch() -> None:
|
|
600
|
+
payload = _make_bash_payload(
|
|
601
|
+
'rm -rf "C:/Users/jon/AppData/Local/Temp/bugteam-pr-58-20260424071040/pr-58/worktree/.bugteam-tmp"'
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
result = _run_rm_hook(payload)
|
|
605
|
+
|
|
606
|
+
assert result.stdout.strip() == ""
|
|
607
|
+
assert result.returncode == 0
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def test_rm_rf_asks_when_leading_cd_target_contains_command_substitution_dollar_parenthesis() -> None:
|
|
611
|
+
payload = _make_bash_payload(
|
|
612
|
+
'cd "/tmp/$(rm -rf ~/.ssh)" && ls'
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
result = _run_rm_hook(payload)
|
|
616
|
+
|
|
617
|
+
response = json.loads(result.stdout)
|
|
618
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def test_rm_rf_asks_when_leading_cd_target_contains_backtick_command_substitution() -> None:
|
|
622
|
+
payload = _make_bash_payload(
|
|
623
|
+
'cd "/tmp/`rm -rf ~/.ssh`" && rm -rf .bugteam-tmp'
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
result = _run_rm_hook(payload)
|
|
627
|
+
|
|
628
|
+
response = json.loads(result.stdout)
|
|
629
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def test_rm_rf_asks_when_leading_cd_target_contains_variable_expansion() -> None:
|
|
633
|
+
payload = _make_bash_payload(
|
|
634
|
+
'cd "/tmp/$SNEAKY_VAR" && rm -rf .bugteam-tmp'
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
result = _run_rm_hook(payload)
|
|
638
|
+
|
|
639
|
+
response = json.loads(result.stdout)
|
|
640
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def test_rm_rf_asks_when_leading_cd_adjacent_quoted_strings_resolve_outside_ephemeral() -> None:
|
|
644
|
+
payload = _make_bash_payload(
|
|
645
|
+
'cd "/tmp/a""/../../etc" && rm -rf .'
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
result = _run_rm_hook(payload)
|
|
649
|
+
|
|
650
|
+
response = json.loads(result.stdout)
|
|
651
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def test_rm_rf_asks_when_leading_cd_adjacent_quoted_strings_use_mixed_quotes() -> None:
|
|
655
|
+
payload = _make_bash_payload(
|
|
656
|
+
"""cd "/tmp/a"'/../../etc' && rm -rf ."""
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
result = _run_rm_hook(payload)
|
|
660
|
+
|
|
661
|
+
response = json.loads(result.stdout)
|
|
662
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
663
|
+
|
|
664
|
+
|
|
268
665
|
def test_rm_rf_asks_when_target_is_bare_tmp_root() -> None:
|
|
269
666
|
payload = _make_bash_payload("rm -rf /tmp")
|
|
270
667
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Tests for question_to_user_enforcer hook response shape and detection logic."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
HOOK_SCRIPT_PATH = os.path.join(
|
|
9
|
+
os.path.dirname(__file__), "question_to_user_enforcer.py"
|
|
10
|
+
)
|
|
11
|
+
_HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
|
|
12
|
+
_HOOKS_ROOT = os.path.join(_HOOKS_DIR, "..")
|
|
13
|
+
if _HOOKS_DIR not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIR)
|
|
15
|
+
if _HOOKS_ROOT not in sys.path:
|
|
16
|
+
sys.path.insert(0, _HOOKS_ROOT)
|
|
17
|
+
from config.messages import USER_FACING_ASKUSERQUESTION_NOTICE
|
|
18
|
+
|
|
19
|
+
CLEAN_DECLARATIVE_MESSAGE = "I applied the rename across both files. The tests pass."
|
|
20
|
+
TRAILING_QUESTION_MESSAGE = (
|
|
21
|
+
"I applied the rename across both files. Should this also propagate to the docs?"
|
|
22
|
+
)
|
|
23
|
+
WANT_ME_TO_MESSAGE = "I finished the refactor.\n\nWant me to open the PR now?"
|
|
24
|
+
SHOULD_I_STATEMENT_MESSAGE = (
|
|
25
|
+
"The diff is ready.\n\nShould I proceed with committing it."
|
|
26
|
+
)
|
|
27
|
+
FENCED_CODE_QUESTION_MESSAGE = (
|
|
28
|
+
"Here is the diagnostic snippet:\n\n"
|
|
29
|
+
"```python\n"
|
|
30
|
+
"print('does this work?')\n"
|
|
31
|
+
"```\n\n"
|
|
32
|
+
"The snippet confirms the behavior."
|
|
33
|
+
)
|
|
34
|
+
RHETORICAL_MIDDLE_MESSAGE = (
|
|
35
|
+
"Consider the failure mode.\n\n"
|
|
36
|
+
"What happens if the queue is empty? The handler short-circuits cleanly.\n\n"
|
|
37
|
+
"That covers the edge case."
|
|
38
|
+
)
|
|
39
|
+
QUESTION_WITH_TRAILING_DOUBLE_QUOTE_MESSAGE = (
|
|
40
|
+
'I renamed the flag.\n\nDid you mean "enable_fast_path?"'
|
|
41
|
+
)
|
|
42
|
+
QUESTION_WITH_TRAILING_SINGLE_QUOTE_MESSAGE = (
|
|
43
|
+
"I renamed the flag.\n\nDid you mean 'enable_fast_path?'"
|
|
44
|
+
)
|
|
45
|
+
QUESTION_WITH_TRAILING_PAREN_MESSAGE = (
|
|
46
|
+
"I finished the refactor.\n\nShould I also bump the version (minor or patch?)"
|
|
47
|
+
)
|
|
48
|
+
QUESTION_WITH_TRAILING_BRACKET_MESSAGE = (
|
|
49
|
+
"I finished the refactor.\n\nShould I also bump the version [minor or patch?]"
|
|
50
|
+
)
|
|
51
|
+
QUESTION_WITH_TRAILING_SPACE_MESSAGE = (
|
|
52
|
+
"I finished the refactor.\n\nShould I also bump the version? "
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_hook_with_payload(payload: dict) -> subprocess.CompletedProcess:
|
|
57
|
+
hook_input_payload = json.dumps(payload)
|
|
58
|
+
return subprocess.run(
|
|
59
|
+
[sys.executable, HOOK_SCRIPT_PATH],
|
|
60
|
+
input=hook_input_payload,
|
|
61
|
+
capture_output=True,
|
|
62
|
+
text=True,
|
|
63
|
+
check=False,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_hook_with_message(assistant_message: str) -> subprocess.CompletedProcess:
|
|
68
|
+
return run_hook_with_payload({"last_assistant_message": assistant_message})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_clean_declarative_message_passes_through():
|
|
72
|
+
completed_process = run_hook_with_message(CLEAN_DECLARATIVE_MESSAGE)
|
|
73
|
+
assert completed_process.returncode == 0
|
|
74
|
+
assert completed_process.stdout == ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_final_paragraph_ending_with_question_mark_emits_block():
|
|
78
|
+
completed_process = run_hook_with_message(TRAILING_QUESTION_MESSAGE)
|
|
79
|
+
assert completed_process.returncode == 0
|
|
80
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
81
|
+
assert parsed_response["decision"] == "block"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_want_me_to_preamble_in_final_paragraph_emits_block():
|
|
85
|
+
completed_process = run_hook_with_message(WANT_ME_TO_MESSAGE)
|
|
86
|
+
assert completed_process.returncode == 0
|
|
87
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
88
|
+
assert parsed_response["decision"] == "block"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_should_i_preamble_without_question_mark_emits_block():
|
|
92
|
+
completed_process = run_hook_with_message(SHOULD_I_STATEMENT_MESSAGE)
|
|
93
|
+
assert completed_process.returncode == 0
|
|
94
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
95
|
+
assert parsed_response["decision"] == "block"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_question_mark_inside_fenced_code_block_passes_through():
|
|
99
|
+
completed_process = run_hook_with_message(FENCED_CODE_QUESTION_MESSAGE)
|
|
100
|
+
assert completed_process.returncode == 0
|
|
101
|
+
assert completed_process.stdout == ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_stop_hook_active_flag_passes_through():
|
|
105
|
+
completed_process = run_hook_with_payload(
|
|
106
|
+
{
|
|
107
|
+
"last_assistant_message": TRAILING_QUESTION_MESSAGE,
|
|
108
|
+
"stop_hook_active": True,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
assert completed_process.returncode == 0
|
|
112
|
+
assert completed_process.stdout == ""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_rhetorical_middle_paragraph_with_declarative_final_passes_through():
|
|
116
|
+
completed_process = run_hook_with_message(RHETORICAL_MIDDLE_MESSAGE)
|
|
117
|
+
assert completed_process.returncode == 0
|
|
118
|
+
assert completed_process.stdout == ""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_block_response_json_shape():
|
|
122
|
+
completed_process = run_hook_with_message(TRAILING_QUESTION_MESSAGE)
|
|
123
|
+
assert completed_process.returncode == 0
|
|
124
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
125
|
+
assert parsed_response["decision"] == "block"
|
|
126
|
+
assert "AskUserQuestion" in parsed_response["reason"]
|
|
127
|
+
assert parsed_response["systemMessage"] == USER_FACING_ASKUSERQUESTION_NOTICE
|
|
128
|
+
assert parsed_response["suppressOutput"] is True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_question_followed_by_double_quote_emits_block():
|
|
132
|
+
completed_process = run_hook_with_message(QUESTION_WITH_TRAILING_DOUBLE_QUOTE_MESSAGE)
|
|
133
|
+
assert completed_process.returncode == 0
|
|
134
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
135
|
+
assert parsed_response["decision"] == "block"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_question_followed_by_single_quote_emits_block():
|
|
139
|
+
completed_process = run_hook_with_message(QUESTION_WITH_TRAILING_SINGLE_QUOTE_MESSAGE)
|
|
140
|
+
assert completed_process.returncode == 0
|
|
141
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
142
|
+
assert parsed_response["decision"] == "block"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_question_followed_by_closing_paren_emits_block():
|
|
146
|
+
completed_process = run_hook_with_message(QUESTION_WITH_TRAILING_PAREN_MESSAGE)
|
|
147
|
+
assert completed_process.returncode == 0
|
|
148
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
149
|
+
assert parsed_response["decision"] == "block"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_question_followed_by_closing_bracket_emits_block():
|
|
153
|
+
completed_process = run_hook_with_message(QUESTION_WITH_TRAILING_BRACKET_MESSAGE)
|
|
154
|
+
assert completed_process.returncode == 0
|
|
155
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
156
|
+
assert parsed_response["decision"] == "block"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_question_followed_by_trailing_space_emits_block():
|
|
160
|
+
completed_process = run_hook_with_message(QUESTION_WITH_TRAILING_SPACE_MESSAGE)
|
|
161
|
+
assert completed_process.returncode == 0
|
|
162
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
163
|
+
assert parsed_response["decision"] == "block"
|