Scale typo gates with length and refuse destructive auto-runs

Similarity requirements now scale with word length (typo.floor_ok:
single edit or ratio >= max(0.6, 1 - 3/max(len,3)), first char kept;
zsh spdist / nushell did_you_mean derivation in the source). The
fixed-cutoff gates are swapped: history_resolver._similar ->
floor_ok with the divergence cap deleted (every diverged token is
gated per-token, not counted), learned.guess_from_path and the help
resolver candidate gates -> floor_ok, so _TOKEN_CUTOFF,
_MAX_DIVERGED, GUESS_CUTOFF, _CUTOFF and their difflib plumbing are
gone; unique-survivor, no-op and which/'/'/'.'/flag guards kept.

New thefuck/danger.py is_dangerous(script) parses via bashlex
directly and fail-safes to True when bashlex is unavailable or the
script refuses to parse (the flat fallback is head-only); with a
tree it matches rm/rmdir recursive flags, dd/mkfs*/shred/wipefs/
mkswap heads, git push --force/-f (not --force-with-lease),
chmod/chown -R with a 777-style mode, kill -9, fork-bomb shapes,
pipe-to-shell tails and file redirects outside /tmp and /dev/null.
fix_command checks it before ANY auto-run, learned-db exact hits
included: dangerous candidates fall through to rules+ask.

Test-migration inventory (authorized semantic inversions):
- tests/resolvers/test_history_resolver.py: declines-3-diverged ->
  corrects (cap deleted); 0.8-cutoff boundary arithmetic re-based to
  floor boundaries (len-3 0.6 / len-10 0.7 / len-30 0.9);
  _TOKEN_CUTOFF import removed with the constant; declines-just-
  below-cutoff re-based to the len-20 floor 0.85; added a 17-char
  below-floor decline.
- tests/test_learned.py: returns-none-below-cutoff re-based to a
  same-first-char below-floor pair (0.6 < len-10 floor 0.7); added a
  ratio-0.7 acceptance pin; single-edit-under-cutoff renamed.
- tests/resolvers/test_help_resolver.py: transposition comments
  re-based to the floor; added a below-floor subcommand decline.
- tests/entrypoints/test_fix_command_learned.py: exact-learned-wins
  fixture's 'git push --force' correction (now correctly refused)
  replaced by benign scripts; mock_learned stubs danger benign for
  platform-neutral auto-apply tests; added TestDangerOverride
  (real module, all four sources: reaches select_command, nothing
  auto-runs).
This commit is contained in:
Alexander
2026-09-14 20:42:26 +02:00
parent 8d2f2fa11c
commit 7d89277717
12 changed files with 469 additions and 87 deletions
+45 -2
View File
@@ -1,5 +1,6 @@
import pytest
from mock import Mock, patch
from thefuck import danger as real_danger
from thefuck.entrypoints.fix_command import fix_command
from thefuck.types import CorrectedCommand
@@ -8,6 +9,14 @@ from thefuck.types import CorrectedCommand
def mock_learned(monkeypatch):
state = {"correction": None, "guess": None, "history": None,
"help": None, "recordings": [], "calls": []}
# The real gate fail-safes to True without bashlex, which would
# make every auto-apply test platform-dependent; the danger
# override tests re-install the real module.
fake_danger = Mock()
fake_danger.is_dangerous.return_value = False
monkeypatch.setattr(
"thefuck.entrypoints.fix_command.danger", fake_danger
)
def fake_get_correction(script):
state["calls"].append("correction")
@@ -121,7 +130,7 @@ class TestGuessAutoApply(object):
def test_exact_learned_wins_over_guess(
self, mock_learned, known_args, settings, monkeypatch
):
mock_learned["correction"] = "git push --force origin main"
mock_learned["correction"] = "git push origin dev"
mock_learned["guess"] = "git push origin main"
with patch("thefuck.types.CorrectedCommand.run"), patch(
@@ -129,7 +138,7 @@ class TestGuessAutoApply(object):
) as mock_show:
fix_command(known_args)
shown_cmd = mock_show.call_args[0][0]
assert shown_cmd.script == "git push --force origin main"
assert shown_cmd.script == "git push origin dev"
assert mock_learned["recordings"] == []
@@ -214,6 +223,40 @@ class TestResolverAutoApply(object):
]
class TestDangerOverride(object):
@pytest.mark.parametrize(
"hit_source", ["correction", "history", "guess", "help"])
def test_dangerous_hit_asks_instead_of_auto_running(
self, mock_learned, known_args, settings, monkeypatch, hit_source
):
# The real gate: a danger-flagged candidate from ANY source —
# a seeded learned-db exact hit included — reaches
# select_command and nothing auto-runs. The abort via the
# mocked selection keeps the pin free of terminal IO.
monkeypatch.setattr(
"thefuck.entrypoints.fix_command.danger", real_danger
)
mock_learned[hit_source] = "rm -rf /"
select = Mock(return_value=None)
monkeypatch.setattr(
"thefuck.entrypoints.fix_command.get_corrected_commands",
lambda _: iter([]),
)
monkeypatch.setattr(
"thefuck.entrypoints.fix_command.select_command", select
)
with patch("thefuck.types.CorrectedCommand.run") as mock_run, patch(
"thefuck.logs.show_corrected_command"
) as mock_show:
with pytest.raises(SystemExit):
fix_command(known_args)
select.assert_called_once()
mock_run.assert_not_called()
mock_show.assert_not_called()
class TestRecordOnSelection(object):
def test_records_user_selection(
self, mock_learned, known_args, settings, monkeypatch