[FIX] todo : onze menus sans miette, et un contrôle qui les voit

Onze sous-menus n'avaient pas d'étiquette de fil d'Ariane. Leur segment
manquait à l'en-tête, et la télémétrie les traitait comme des COMMANDES : ils
apparaissaient en feuille, sous leur nom de méthode brut.

Le contrôle qui devait l'attraper cherchait dans la table de répartition du
menu Exécution, donc il ne voyait que les sous-menus atteints depuis là — un
menu ouvert depuis ailleurs, ou défini dans un mixin, n'était jamais examiné.
Il cherche maintenant par ce que le code APPELLE, dans tout le paquet, et
trois exemptions périmées ont disparu avec lui. Cinq écrans restent exemptés
et disent pourquoi : ce sont des actions qui posent une question.

--- EN ---

Eleven submenus had no breadcrumb label. Their segment was missing from the
header, and telemetry treated them as COMMANDS: they showed up as leaves,
under their raw method name.

The check meant to catch this looked in the Execute menu's dispatch table, so
it only saw submenus reached from there — a menu opened from elsewhere, or
defined in a mixin, was never examined. It now searches by what the code
CALLS, across the whole package, and three stale exemptions went with it. Five
screens stay exempt and say why: they are actions that ask a question.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-09-09 04:42:39 -04:00
parent 2df3be2df8
commit 314ab134d7
2 changed files with 78 additions and 13 deletions

View file

@ -575,6 +575,9 @@ class TODO(
"prompt_execute": "Execute",
"prompt_assistant": "Assistant",
"prompt_assistant_llm": "LLM",
"_llm_servers": "Servers",
"_llm_search": "Search",
"_llm_search_remote": "Over SSH",
"prompt_install": "Install",
"prompt_execute_function": "Automation",
"prompt_execute_code": "Code",
@ -584,7 +587,10 @@ class TODO(
"prompt_execute_doc": "Doc",
"prompt_execute_git": "Git",
"prompt_execute_git_local_server": "Git local server",
"_prompt_git_server_actions": "Actions",
"prompt_execute_gpt_code": "GPT code",
"_prompt_claude_configs": "Claude configs",
"prompt_execute_claude_plugins": "Plugins",
"prompt_claude_sessions": "Claude Code",
"prompt_execute_process": "Process",
"prompt_execute_instance": "Run",
@ -594,6 +600,11 @@ class TODO(
"prompt_execute_deploy_ssh": "SSH",
"prompt_execute_qemu": "QEMU/KVM",
"prompt_execute_proxmox": "Proxmox VE",
"prompt_execute_vpn": "VPN",
"prompt_execute_network": "Network",
"prompt_execute_security": "Security",
"prompt_execute_test": "Test",
"prompt_execute_longtest": "Long test",
"prompt_configuration": "Configuration",
}

View file

@ -513,15 +513,28 @@ class TestMenuLabels(unittest.TestCase):
Sans elle, `_menu_header` n'affiche pas le segment et
`todo_telemetry.build_code_tree` traite le menu comme une COMMANDE :
il apparaît en feuille, sous son nom de méthode brut. Trois menus en
souffrent déjà — la liste est figée ici pour que le nombre ne grandisse
pas, pas pour bénir ce qu'elle contient.
il apparaît en feuille, sous son nom de méthode brut.
Les menus se trouvent par ce qu'ils APPELLENT — `fill_help_info` ou
`_menu_header` — dans tout le paquet, et non par la table de répartition
du menu Exécution. Chercher là ne voyait que les sous-menus atteints
depuis cette table : un menu ouvert depuis ailleurs, ou défini dans un
mixin, n'était jamais examiné, et c'est ainsi que le sous-menu VPN a
passé le contrôle sans étiquette.
Cinq méthodes sont exemptées, et pour la même raison : ce sont des
ACTIONS qui posent une question — un choix de méthode d'installation, un
« aller plus loin » après un rapport — et non des écrans où l'on
navigue. Leur donner un segment mettrait une miette sur une invite
passagère.
"""
KNOWN_MISSING = {
"prompt_execute_test",
"prompt_execute_network",
"prompt_execute_security",
ECRANS_EXEMPTES = {
"_analyse_follow_up",
"rtk_install",
"generate_config_from_preconfiguration",
"debug_ide",
"execute_odoo_upgrade",
}
def setUp(self):
@ -553,19 +566,60 @@ class TestMenuLabels(unittest.TestCase):
def test_analyse_menu_has_a_breadcrumb_label(self):
self.assertIn("prompt_execute_analyse", self.labels)
def test_no_new_menu_forgets_its_label(self):
submenus = {
method
for method in self.dispatched
if method.startswith("prompt_execute_")
@staticmethod
def _menus_du_paquet():
"""Toute méthode qui dessine un menu, dans tout script/todo/*.py.
Une méthode dessine un menu quand elle appelle `fill_help_info` ou
`_menu_header` : c'est par là que passe l'en-tête, donc c'est là que
l'étiquette manque ou non. Les deux fonctions elles-mêmes sortent."""
trouves = set()
for chemin in sorted(TODO_DIR.glob("*.py")):
arbre = ast.parse(chemin.read_text(encoding="utf-8"))
for noeud in ast.walk(arbre):
if not isinstance(noeud, ast.FunctionDef):
continue
appels = {
c.func.attr
for c in ast.walk(noeud)
if isinstance(c, ast.Call)
and isinstance(c.func, ast.Attribute)
}
missing = submenus - self.labels - self.KNOWN_MISSING
if not {"fill_help_info", "_menu_header"} & appels:
continue
if noeud.name in ("fill_help_info", "_menu_header"):
continue
trouves.add(noeud.name)
return trouves
def test_the_package_menus_were_found(self):
"""Le détecteur voit bien des menus : sinon tout passerait."""
menus = self._menus_du_paquet()
self.assertIn("prompt_execute_qemu", menus)
self.assertIn("prompt_execute_vpn", menus)
self.assertGreater(len(menus), 20)
def test_no_new_menu_forgets_its_label(self):
missing = self._menus_du_paquet() - self.labels - self.ECRANS_EXEMPTES
self.assertEqual(
missing,
set(),
f"menus sans étiquette dans _MENU_LABELS : {sorted(missing)}",
)
def test_the_vpn_submenu_leaves_a_crumb(self):
"""Le cas nommé : il était le seul menu invisible au contrôle."""
self.assertIn("prompt_execute_vpn", self.labels)
def test_no_stale_exemption(self):
"""Une exemption qui ne nomme plus un menu est à retirer."""
fantomes = self.ECRANS_EXEMPTES - self._menus_du_paquet()
self.assertEqual(fantomes, set())
def test_an_exemption_is_never_also_labelled(self):
"""Exempter ET étiqueter dirait deux choses opposées du même écran."""
self.assertEqual(self.ECRANS_EXEMPTES & self.labels, set())
if __name__ == "__main__":
unittest.main()