From 4e0bf7cc8b032627c5062e73dad74205957b449e Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 19 Aug 2026 05:16:48 -0400 Subject: [PATCH] [FIX] migration: the countdown names the answer the prompt offered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit « ⏱ → d » named a reply that was not on the menu. The prompt reads « Enter = delete, after saving them, k = keep » and mentions « d » nowhere, so the line asking to be trusted with a decision described it in a vocabulary the reader had never been given. The decision itself was right — Enter deletes, and that is what happened. Only its report was written from the inside. It now says Enter, in the migration's language, with the value in brackets for whoever wants the precision. What the caller receives is unchanged. --- FR --- [FIX] migration : le compte à rebours nomme la réponse que l'invite offrait « ⏱ → d » nommait une réponse qui n'était pas au menu. L'invite annonce « Entrée = effacer, après les avoir sauvegardés, k = garder » et ne mentionne « d » nulle part : la ligne qui demandait qu'on lui confie une décision la décrivait donc dans un vocabulaire jamais montré. La décision, elle, était juste — Entrée efface, et c'est ce qui a eu lieu. Seul son compte rendu était écrit du dedans. Il dit maintenant Entrée, dans la langue de la migration, la valeur entre parenthèses pour qui veut la précision. Ce que l'appelant reçoit ne change pas. Assisted-by: Claude Opus 5 --- script/todo/auto_ask.py | 20 +++++++++++- test/test_prompt_defaults.py | 61 ++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/script/todo/auto_ask.py b/script/todo/auto_ask.py index eefee6e..d373ce7 100644 --- a/script/todo/auto_ask.py +++ b/script/todo/auto_ask.py @@ -23,6 +23,18 @@ derrière lui un `input()` toujours bloqué, qui volerait la frappe suivante. import os import sys +sys.path.append( + os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) +) + +try: + from script.todo.todo_i18n import t +except Exception: # pragma: no cover - repli si i18n indisponible + + def t(key: str) -> str: + return key + + ENV_ENABLED = "ERPLIBRE_AUTO_EXECUTE" ENV_DELAY = "ERPLIBRE_AUTO_DELAY" DEFAULT_DELAY = 5 @@ -90,7 +102,13 @@ def ask(prompt, default="", seconds=None): # lisible tout de suite et rend justement une ligne vide ; sans # ceci, le défaut ne servirait jamais là. return answer or default - print(f" ⏱ → {default or '(default)'}") + # « Entrée », parce que c'est ce que l'invite a proposé. Elle annonce + # « Entrée = effacer, k = garder » et ne mentionne nulle part « d » : + # afficher la valeur brute nommait donc une réponse qui n'était pas au + # menu, et l'on se demandait ce que l'outil venait de décider. La + # valeur reste, entre parenthèses, pour qui veut la précision. + detail = f" ({default})" if default else "" + print(f" ⏱ → {t('Enter')}{detail}") return default diff --git a/test/test_prompt_defaults.py b/test/test_prompt_defaults.py index f9f6f71..76427a3 100644 --- a/test/test_prompt_defaults.py +++ b/test/test_prompt_defaults.py @@ -163,6 +163,67 @@ class TestEnterMeansTheDefault(EnvCase): self.assertEqual(os.environ.get(auto_ask.ENV_ENABLED), "1") +class TestTheCountdownSpeaksThePromptsLanguage(EnvCase): + """« ⏱ → d » nommait une réponse qui n'était PAS au menu. + + L'invite annonce « Entrée = effacer, k = garder » : elle ne mentionne + « d » nulle part. Afficher la valeur brute obligeait donc à deviner ce + que l'outil venait de décider — alors que la décision était exactement + celle qu'Entrée promettait. + """ + + def setUp(self): + super().setUp() + from script.todo import todo_i18n + + self.addCleanup( + setattr, todo_i18n, "_current_lang", todo_i18n._current_lang + ) + todo_i18n._current_lang = "en" + + def tick(self, default): + """Forcer le chemin du DÉLAI, celui qui affiche la ligne.""" + import select + + original = select.select + select.select = lambda r, w, x, t: ([], [], []) + self.addCleanup(setattr, select, "select", original) + auto_ask.export(True, 0.01) + out = io.StringIO() + with contextlib.redirect_stdout(out): + valeur = auto_ask.ask("q : ", default=default) + return valeur, out.getvalue() + + def test_it_names_what_the_prompt_offered(self): + _valeur, texte = self.tick("d") + self.assertIn("Enter", texte) + + def test_the_raw_value_stays_for_precision(self): + # La nommer reste utile : deux invites voisines peuvent avoir des + # défauts différents, et l'on relit ces lignes après coup. + _valeur, texte = self.tick("d") + self.assertIn("(d)", texte) + + def test_an_empty_default_shows_no_parentheses(self): + # « Entrée () » n'aurait aucun sens : il n'y a rien à préciser. + _valeur, texte = self.tick("") + self.assertIn("Enter", texte) + self.assertNotIn("(", texte) + + def test_the_answer_itself_is_unchanged(self): + # L'affichage seul change : ce que la migration reçoit reste la + # valeur, sinon toutes les invites changeraient de comportement. + valeur, _texte = self.tick("d") + self.assertEqual(valeur, "d") + + def test_it_follows_the_language_of_the_migration(self): + from script.todo import todo_i18n + + todo_i18n._current_lang = "fr" + _valeur, texte = self.tick("d") + self.assertIn("Entrée", texte) + + class TestNoPromptOfTheMigrationCanHang(unittest.TestCase): """Un `input()` nu ne sait rien du mode auto : il attend, pour toujours.