Compose fixes to a fixpoint and learn only from success

New thefuck/compose.py resolve() iterates the auto-run sources
(learned-db exact, error-suggestion, history, path guess, help) per
round on the previous round's script until stable, a revisit aborts
or no source has a safe candidate; dangerous candidates are declined
mid-round and consultation continues. Output-aware sources get a
synthesized Command wrapping the round's script with the original
failure's output. Zero accepted steps return None so the original
failing script is never auto-executed; no round cap (termination
contract: visited-set + fixed-range/word-count-preserving sources,
stated in the module docstring; a compose-local memo acts only as a
determinism tripwire).

fix_command runs the inline chain through the composer; under
settings.repeat or a non-nushell shell it records immediately before
the run unless the only step replayed a learned-db exact hit; under
nushell it closes the learned db first and records on returncode 0,
forgets on failure (poison self-heal; CorrectedCommand.run now
stashes the nu returncode on the instance).

learned.forget(original, corrected) is the exact inverse of record
(cmd: + matching-count word:/part: deletes, assembly failures are a
documented no-op); learned.close_db is idempotent and atexit now
registers the stable close_db method exactly once across
close/reopen cycles.

Test migration (authorized): test_fix_command_learned.py rebased to
composer semantics (per-round consultation incl. error-suggestion;
input-aware fakes so round 2 declines, preserving the recordings==[]
pin in test_exact_learned_wins_over_guess).
This commit is contained in:
Alexander
2026-09-14 21:02:14 +02:00
parent 58f6cca9cb
commit ab49450467
8 changed files with 895 additions and 94 deletions
+89
View File
@@ -261,6 +261,95 @@ class TestClear(object):
assert learned.get_correction("git psuh") is None
class TestForget(object):
def test_removes_cmd_and_derived_part_entries(self, learned):
learned.record("git psuh origin main", "git push origin main")
learned.forget("git psuh origin main", "git push origin main")
assert "cmd:git psuh origin main" not in learned.db
assert "part:git:psuh" not in learned.db
def test_removes_cmd_and_derived_word_entry(self, learned):
learned.record("pyhton script.py", "python script.py")
learned.forget("pyhton script.py", "python script.py")
assert "cmd:pyhton script.py" not in learned.db
assert "word:pyhton" not in learned.db
def test_removes_every_diverged_position(self, learned):
learned.record("gti comit -m msg", "git commit -m msg")
learned.forget("gti comit -m msg", "git commit -m msg")
assert "cmd:gti comit -m msg" not in learned.db
assert "word:gti" not in learned.db
assert "part:git:comit" not in learned.db
def test_deletes_rather_than_decrements(self, learned):
for _ in range(3):
learned.record("git psuh", "git push")
learned.forget("git psuh", "git push")
assert "cmd:git psuh" not in learned.db
assert "part:git:psuh" not in learned.db
def test_keeps_unrelated_entries(self, learned):
learned.record("git psuh", "git push")
learned.record("pyhton x.py", "python x.py")
learned.forget("git psuh", "git push")
assert "cmd:pyhton x.py" in learned.db
assert "word:pyhton" in learned.db
def test_mismatched_token_counts_forgets_cmd_only(self, learned):
learned.record("git push", "git push --set-upstream origin main")
learned.forget("git push", "git push --set-upstream origin main")
assert "cmd:git push" not in learned.db
assert not any(k.startswith(("word:", "part:"))
for k in learned.db)
def test_assembly_failure_is_a_noop(self, learned):
# "gti comit" resolves by assembling word:gti alone (no cmd:
# entry of its own); those mappings were validated by an
# earlier success of a different command and must stand.
learned.record("gti push", "git push")
assert learned.get_correction("gti comit") == "git comit"
learned.forget("gti comit", "git comit")
assert "word:gti" in learned.db
assert "cmd:gti comit" not in learned.db
def test_unknown_command_is_a_noop(self, learned):
learned.forget("never recorded", "anything at all")
assert len(learned.db) == 0
class TestCloseDb(object):
def test_close_sets_db_none_and_is_idempotent(self, learned):
learned.record("git psuh", "git push")
learned.close_db()
assert learned._db is None
learned.close_db()
assert learned._db is None
def test_reopen_after_close_still_finds_entries(
self, tmp_path, monkeypatch):
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
lc = LearnedCorrections()
lc.record("git psuh", "git push")
first_handle = lc._db
lc.close_db()
assert lc.get_correction("git psuh") == "git push"
assert lc._db is not None
assert lc._db is not first_handle
def test_atexit_binds_close_db_once_across_reopens(
self, tmp_path, monkeypatch):
registrations = []
monkeypatch.setattr(
"thefuck.learned.atexit.register", registrations.append)
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
lc = LearnedCorrections()
lc.record("git psuh", "git push")
lc.close_db()
lc.record("gti comit -m msg", "git commit -m msg")
lc.close_db()
assert registrations == [lc.close_db]
class TestRoundTrip(object):
def test_record_then_match(self, learned):
learned.record("docker bilud .", "docker build .")