Files
thefuck/tests/test_learned.py
T
Alexander 1d3e4b7e8a Guess mistyped commands from $PATH before asking the user
When no learned correction matches, fuzzy-match the mistyped token
against executables from $PATH and shell aliases. A guess is auto-run
and recorded into the learned-corrections db only when it is
unambiguous: the token is not an existing executable, contains no
path separators or extensions, shares its first character with the
candidate, has similarity of at least 0.8, and exactly one candidate
survives. Asking the user stays the last resort.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-13 20:01:22 +02:00

192 lines
7.8 KiB
Python

import pytest
from thefuck.learned import LearnedCorrections
@pytest.fixture
def learned(tmp_path):
lc = LearnedCorrections()
lc._db = {}
return lc
class TestRecord(object):
def test_noop_when_scripts_identical(self, learned):
learned.record("git push", "git push")
assert len(learned.db) == 0
def test_stores_full_command_mapping(self, learned):
learned.record("git psuh origin main", "git push origin main")
assert (
learned.db["cmd:git psuh origin main"]["corrected"]
== "git push origin main"
)
def test_increments_count_on_repeat(self, learned):
learned.record("git psuh", "git push")
learned.record("git psuh", "git push")
assert learned.db["cmd:git psuh"]["count"] == 2
def test_updates_timestamp(self, learned):
learned.record("git psuh", "git push")
first_ts = learned.db["cmd:git psuh"]["timestamp"]
learned.record("git psuh", "git push")
assert learned.db["cmd:git psuh"]["timestamp"] >= first_ts
def test_stores_command_word_replacement(self, learned):
learned.record("pyhton script.py", "python script.py")
assert learned.db["word:pyhton"]["replacement"] == "python"
def test_stores_subcommand_replacement(self, learned):
learned.record("git psuh origin main", "git push origin main")
assert learned.db["part:git:psuh"]["replacement"] == "push"
def test_stores_multiple_word_diffs(self, learned):
learned.record("gti comit -m msg", "git commit -m msg")
assert learned.db["word:gti"]["replacement"] == "git"
assert learned.db["part:git:comit"]["replacement"] == "commit"
def test_no_word_level_when_lengths_differ(self, learned):
learned.record("git push", "git push --set-upstream origin main")
assert "cmd:git push" in learned.db
assert not any(
k.startswith("word:") or k.startswith("part:") for k in learned.db
)
def test_updates_correction_on_changed_choice(self, learned):
learned.record("apt install vim", "sudo apt install vim")
learned.record("apt install vim", "apt-get install vim")
assert learned.db["cmd:apt install vim"]["corrected"] == "apt-get install vim"
assert learned.db["cmd:apt install vim"]["count"] == 2
class TestGetCorrection(object):
def test_exact_full_command_match(self, learned):
learned.record("git psuh origin main", "git push origin main")
assert learned.get_correction("git psuh origin main") == "git push origin main"
def test_generalises_subcommand_to_different_args(self, learned):
learned.record("git psuh origin main", "git push origin main")
assert learned.get_correction("git psuh origin dev") == "git push origin dev"
def test_generalises_command_word(self, learned):
learned.record("pyhton script.py", "python script.py")
assert learned.get_correction("pyhton other.py") == "python other.py"
def test_combined_command_and_subcommand(self, learned):
learned.record("gti comit -m msg", "git commit -m msg")
assert learned.get_correction('gti comit -m "other"') == 'git commit -m "other"'
def test_returns_none_when_no_match(self, learned):
assert learned.get_correction("totally unknown cmd") is None
def test_returns_none_for_empty_script(self, learned):
assert learned.get_correction("") is None
def test_prefers_full_command_over_word_level(self, learned):
learned.record("git psuh origin main", "git push origin main")
learned.db["cmd:git psuh origin main"]["corrected"] = (
"git push --force origin main"
)
assert (
learned.get_correction("git psuh origin main")
== "git push --force origin main"
)
def test_word_level_only_replaces_known_tokens(self, learned):
learned.record("git psuh origin main", "git push origin main")
result = learned.get_correction("git psuh origin dev")
assert result == "git push origin dev"
def test_cross_resolve_command_and_part(self, learned):
"""When both cmd word and subcommand are typos, parts stored
under the corrected cmd name still resolve."""
learned.record("gti psuh origin main", "git push origin main")
assert learned.get_correction("gti psuh origin dev") == "git push origin dev"
def test_single_word_command(self, learned):
learned.record("sl", "ls")
assert learned.get_correction("sl") == "ls"
class TestGuessFromPath(object):
@pytest.fixture
def path_bins(self, monkeypatch):
def setup(executables, existing=()):
monkeypatch.setattr('thefuck.learned.get_all_executables',
lambda: list(executables))
monkeypatch.setattr('thefuck.learned.which',
lambda token: token in existing)
return setup
def test_guesses_unique_close_match(self, learned, path_bins):
path_bins(executables=['clear', 'grep', 'sed'])
assert learned.guess_from_path('cler') == 'clear'
def test_keeps_arguments(self, learned, path_bins):
path_bins(executables=['python', 'pydoc', 'grep'])
assert (learned.guess_from_path('pyhton script.py')
== 'python script.py')
def test_returns_none_when_ambiguous(self, learned, path_bins):
path_bins(executables=['clear', 'clean'])
assert learned.guess_from_path('clea') is None
def test_returns_none_when_token_is_executable(self, learned, path_bins):
path_bins(executables=['clear'], existing=['clear'])
assert learned.guess_from_path('clear') is None
def test_returns_none_for_path_like_token(self, learned, path_bins):
path_bins(executables=['git', 'grep', 'sed'])
assert learned.guess_from_path('./gti push') is None
def test_returns_none_for_token_with_extension(self, learned, path_bins):
path_bins(executables=['git', 'grep', 'sed'])
assert learned.guess_from_path('giti.py x') is None
def test_returns_none_when_first_char_differs(self, learned, path_bins):
path_bins(executables=['top'])
assert learned.guess_from_path('htop') is None
def test_returns_none_below_cutoff(self, learned, path_bins):
path_bins(executables=['grep', 'sed', 'awk'])
assert learned.guess_from_path('total') is None
def test_guesses_after_sudo(self, learned, path_bins):
path_bins(executables=['clear', 'grep', 'sed'])
assert learned.guess_from_path('sudo cler') == 'sudo clear'
def test_returns_none_for_empty_script(self, learned, path_bins):
path_bins(executables=['git'])
assert learned.guess_from_path('') is None
class TestClear(object):
def test_removes_all_entries(self, learned):
learned.record("git psuh", "git push")
learned.record("pyhton x.py", "python x.py")
learned.clear()
assert len(learned.db) == 0
def test_no_matches_after_clear(self, learned):
learned.record("git psuh", "git push")
learned.clear()
assert learned.get_correction("git psuh") is None
class TestRoundTrip(object):
def test_record_then_match(self, learned):
learned.record("docker bilud .", "docker build .")
assert learned.get_correction("docker bilud .") == "docker build ."
def test_record_then_generalise(self, learned):
learned.record("docker bilud -t foo .", "docker build -t foo .")
assert (
learned.get_correction("docker bilud -t bar .") == "docker build -t bar ."
)
def test_multiple_distinct_commands(self, learned):
learned.record("git psuh", "git push")
learned.record("pyhton x.py", "python x.py")
assert learned.get_correction("git psuh") == "git push"
assert learned.get_correction("pyhton y.py") == "python y.py"