7d89277717
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).
338 lines
12 KiB
Python
338 lines
12 KiB
Python
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
|
|
|
|
|
|
@pytest.fixture
|
|
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")
|
|
return state["correction"]
|
|
|
|
def fake_history(command):
|
|
state["calls"].append("history")
|
|
return state["history"]
|
|
|
|
def fake_guess(script):
|
|
state["calls"].append("guess")
|
|
return state["guess"]
|
|
|
|
def fake_help(script):
|
|
state["calls"].append("help")
|
|
return state["help"]
|
|
|
|
def fake_record(original, corrected):
|
|
state["recordings"].append((original, corrected))
|
|
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_correction", fake_get_correction
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_history_correction", fake_history
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.guess_from_path", fake_guess
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_help_correction", fake_help
|
|
)
|
|
monkeypatch.setattr("thefuck.entrypoints.fix_command.record", fake_record)
|
|
return state
|
|
|
|
|
|
@pytest.fixture
|
|
def known_args():
|
|
return Mock(
|
|
force_command="git psuh origin main", yes=False, debug=False, repeat=False
|
|
)
|
|
|
|
|
|
class TestLearnedAutoApply(object):
|
|
def test_auto_applies_learned_correction(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
mock_learned["correction"] = "git push origin main"
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands", lambda _: iter([])
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.select_command", lambda _: None
|
|
)
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run") as mock_run, patch(
|
|
"thefuck.logs.show_corrected_command"
|
|
):
|
|
fix_command(known_args)
|
|
mock_run.assert_called_once()
|
|
|
|
def test_learned_skips_rule_matching(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
mock_learned["correction"] = "git push origin main"
|
|
get_corrected = Mock()
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands", get_corrected
|
|
)
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run"), patch(
|
|
"thefuck.logs.show_corrected_command"
|
|
):
|
|
fix_command(known_args)
|
|
get_corrected.assert_not_called()
|
|
|
|
def test_shows_corrected_command_on_auto_apply(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
mock_learned["correction"] = "git push origin main"
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run"), patch(
|
|
"thefuck.logs.show_corrected_command"
|
|
) as mock_show:
|
|
fix_command(known_args)
|
|
assert mock_show.call_count == 1
|
|
shown_cmd = mock_show.call_args[0][0]
|
|
assert shown_cmd.script == "git push origin main"
|
|
|
|
|
|
class TestGuessAutoApply(object):
|
|
def test_guess_records_and_auto_applies(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
mock_learned["guess"] = "git push origin main"
|
|
get_corrected = Mock()
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands", get_corrected
|
|
)
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run") as mock_run, patch(
|
|
"thefuck.logs.show_corrected_command"
|
|
):
|
|
fix_command(known_args)
|
|
mock_run.assert_called_once()
|
|
get_corrected.assert_not_called()
|
|
assert mock_learned["recordings"] == [
|
|
("git psuh origin main", "git push origin main")
|
|
]
|
|
|
|
def test_exact_learned_wins_over_guess(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
mock_learned["correction"] = "git push origin dev"
|
|
mock_learned["guess"] = "git push origin main"
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run"), patch(
|
|
"thefuck.logs.show_corrected_command"
|
|
) as mock_show:
|
|
fix_command(known_args)
|
|
shown_cmd = mock_show.call_args[0][0]
|
|
assert shown_cmd.script == "git push origin dev"
|
|
assert mock_learned["recordings"] == []
|
|
|
|
|
|
class TestResolverChainOrder(object):
|
|
def test_full_chain_consulted_in_order_when_only_help_hits(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
mock_learned["help"] = "git push origin main"
|
|
get_corrected = Mock()
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands", get_corrected
|
|
)
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run"), patch(
|
|
"thefuck.logs.show_corrected_command"
|
|
):
|
|
fix_command(known_args)
|
|
|
|
assert mock_learned["calls"] == [
|
|
"correction", "history", "guess", "help"
|
|
]
|
|
get_corrected.assert_not_called()
|
|
|
|
@pytest.mark.parametrize("hit_source,expected_calls", [
|
|
("history", ["correction", "history"]),
|
|
("guess", ["correction", "history", "guess"]),
|
|
])
|
|
def test_earlier_hit_stops_the_chain(
|
|
self, mock_learned, known_args, settings, hit_source, expected_calls
|
|
):
|
|
mock_learned[hit_source] = "git push origin main"
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run"), patch(
|
|
"thefuck.logs.show_corrected_command"
|
|
):
|
|
fix_command(known_args)
|
|
|
|
assert mock_learned["calls"] == expected_calls
|
|
|
|
def test_history_receives_command_object(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
history_mock = Mock(return_value=None)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_history_correction", history_mock
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands",
|
|
lambda _: iter([]),
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.select_command", lambda _: None
|
|
)
|
|
|
|
with pytest.raises(SystemExit):
|
|
fix_command(known_args)
|
|
|
|
assert history_mock.call_count == 1
|
|
assert history_mock.call_args[0][0].script == "git psuh origin main"
|
|
|
|
|
|
class TestResolverAutoApply(object):
|
|
@pytest.mark.parametrize("source", ["history", "help"])
|
|
def test_resolver_hit_records_and_auto_applies(
|
|
self, mock_learned, known_args, settings, monkeypatch, source
|
|
):
|
|
mock_learned[source] = "git push origin main"
|
|
get_corrected = Mock()
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands", get_corrected
|
|
)
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run") as mock_run, patch(
|
|
"thefuck.logs.show_corrected_command"
|
|
) as mock_show:
|
|
fix_command(known_args)
|
|
mock_run.assert_called_once()
|
|
get_corrected.assert_not_called()
|
|
assert mock_show.call_args[0][0].script == "git push origin main"
|
|
assert mock_learned["recordings"] == [
|
|
("git psuh origin main", "git push origin main")
|
|
]
|
|
|
|
|
|
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
|
|
):
|
|
selected = CorrectedCommand(
|
|
script="git push origin main", side_effect=None, priority=100
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands",
|
|
lambda _: iter([selected]),
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.select_command", lambda _: selected
|
|
)
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run"):
|
|
fix_command(known_args)
|
|
assert mock_learned["recordings"] == [
|
|
("git psuh origin main", "git push origin main")
|
|
]
|
|
|
|
def test_does_not_record_on_abort(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands", lambda _: iter([])
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.select_command", lambda _: None
|
|
)
|
|
|
|
with pytest.raises(SystemExit):
|
|
fix_command(known_args)
|
|
assert mock_learned["recordings"] == []
|
|
|
|
|
|
class TestFallthrough(object):
|
|
def test_falls_through_to_rules_when_no_learned(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
selected = CorrectedCommand(
|
|
script="git push origin main", side_effect=None, priority=100
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands",
|
|
lambda _: iter([selected]),
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.select_command", lambda _: selected
|
|
)
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run") as mock_run:
|
|
fix_command(known_args)
|
|
mock_run.assert_called_once()
|
|
|
|
def test_falls_through_to_rules_when_all_resolvers_decline(
|
|
self, mock_learned, known_args, settings, monkeypatch
|
|
):
|
|
selected = CorrectedCommand(
|
|
script="git push origin main", side_effect=None, priority=100
|
|
)
|
|
select = Mock(return_value=selected)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.get_corrected_commands",
|
|
lambda _: iter([selected]),
|
|
)
|
|
monkeypatch.setattr(
|
|
"thefuck.entrypoints.fix_command.select_command", select
|
|
)
|
|
|
|
with patch("thefuck.types.CorrectedCommand.run") as mock_run:
|
|
fix_command(known_args)
|
|
mock_run.assert_called_once()
|
|
|
|
assert mock_learned["calls"] == [
|
|
"correction", "history", "guess", "help"
|
|
]
|
|
assert select.call_count == 1
|