"""Tests for hooks/destructive-bash-confirm.py — shlex-based rm -rf parsing.""" from conftest import load_hook hook = load_hook("destructive-bash-confirm.py") SENTINEL = "(unparsed — shell too complex to inspect safely)" # --- detection of flag forms (combined / separate / reordered) ------------- def test_rf_combined_detected(): assert hook.detect_rm_rf("rm -rf build") == ["build"] def test_r_f_separate_detected(): assert hook.detect_rm_rf("rm -r -f build") == ["build"] def test_fr_reordered_detected(): assert hook.detect_rm_rf("rm -fr build") == ["build"] def test_capital_R_detected(): assert hook.detect_rm_rf("rm -Rf build") == ["build"] def test_long_flags_detected(): targets = hook.detect_rm_rf("rm --recursive --force build") assert targets == ["build"] # --- quoted / spaced paths now parse correctly ----------------------------- def test_quoted_path_with_space_parsed(): assert hook.detect_rm_rf('rm -rf "my dir"') == ["my dir"] def test_multiple_targets(): assert hook.detect_rm_rf("rm -rf a b c") == ["a", "b", "c"] def test_double_dash_separates_flags_from_paths(): assert hook.detect_rm_rf("rm -rf -- -weird-name") == ["-weird-name"] # --- not-a-match cases: no modal ------------------------------------------- def test_no_r_returns_none(): assert hook.detect_rm_rf("rm -f file") is None def test_no_f_returns_none(): assert hook.detect_rm_rf("rm -r dir") is None def test_not_rm_returns_none(): assert hook.detect_rm_rf("rmdir foo") is None def test_plain_rm_returns_none(): assert hook.detect_rm_rf("rm file.txt") is None # --- dangerous path banner still fires on parsed targets ------------------- def test_home_var_target_flags_dangerous(): detection = hook.detect_destructive('rm -rf "$HOME/x"') assert detection is not None kind, ctx = detection assert kind == "rm -rf" assert "_banner" in ctx assert "$HOME/x" in ctx["_banner"] def test_root_path_flags_dangerous(): detection = hook.detect_destructive("rm -rf /etc/foo") assert detection is not None _, ctx = detection assert "_banner" in ctx def test_safe_relative_target_no_banner(): detection = hook.detect_destructive("rm -rf build/cache") assert detection is not None _, ctx = detection assert "_banner" not in ctx # --- fail-toward-asking on ambiguity --------------------------------------- def test_compound_command_returns_sentinel(): # `ls && rm -rf foo` — the naive parser missed this; now we ask. assert hook.detect_rm_rf("ls && rm -rf foo") == [SENTINEL] def test_pipeline_returns_sentinel(): assert hook.detect_rm_rf("find . -type d | xargs rm -rf") == [SENTINEL] def test_semicolon_returns_sentinel(): assert hook.detect_rm_rf("cd /tmp; rm -rf junk") == [SENTINEL] def test_command_substitution_returns_sentinel(): assert hook.detect_rm_rf("rm -rf $(echo target)") == [SENTINEL] def test_backtick_substitution_returns_sentinel(): assert hook.detect_rm_rf("rm -rf `echo target`") == [SENTINEL] def test_redirect_returns_sentinel(): assert hook.detect_rm_rf("rm -rf foo > /dev/null") == [SENTINEL] def test_unbalanced_quotes_returns_sentinel(): # shlex.split raises ValueError → ask anyway rather than silently pass. assert hook.detect_rm_rf('rm -rf "unterminated') == [SENTINEL] def test_compound_without_rm_rf_returns_none(): # Compound construct but no dangerous rm — should not fire. assert hook.detect_rm_rf("ls && echo done") is None def test_compound_with_rm_but_no_force_returns_none(): # `&&` present but the rm has no -f, so nothing to flag. assert hook.detect_rm_rf("ls && rm -r dir") is None def test_sentinel_fires_modal_via_detect_destructive(): detection = hook.detect_destructive("ls && rm -rf foo") assert detection is not None kind, ctx = detection assert kind == "rm -rf" assert ctx["targets"] == [SENTINEL]