[FIX] migration: the countdown names the answer the prompt offered

« ⏱ → 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
This commit is contained in:
Mathieu Benoit 2026-08-19 05:16:48 -04:00
parent e214df7329
commit 4e0bf7cc8b
2 changed files with 80 additions and 1 deletions

View file

@ -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

View file

@ -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.