[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
|
|
"""Le menu Execute : les numéros affichés mènent-ils où ils le disent ?
|
|
|
|
|
|
|
|
|
|
|
|
Le menu est écrit deux fois — une f-string qui affiche « [7] … », et une
|
|
|
|
|
|
chaîne d'`elif status == "7"` qui dispatche. Rien ne les reliait : insérer une
|
|
|
|
|
|
entrée au milieu oblige à décaler les deux à la main, et une seule erreur
|
|
|
|
|
|
envoie l'utilisateur dans le mauvais écran sans que rien ne proteste.
|
|
|
|
|
|
|
|
|
|
|
|
Ce test relit les deux et les apparie. Il ne juge pas le contenu du menu :
|
|
|
|
|
|
ajouter, retirer ou réordonner reste libre, tant que l'affichage et le
|
|
|
|
|
|
dispatch racontent la même histoire.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import ast
|
|
|
|
|
|
import re
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
TODO_DIR = Path(__file__).resolve().parent.parent / "script" / "todo"
|
|
|
|
|
|
TODO_PY = TODO_DIR / "todo.py"
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
|
|
|
|
|
|
# « [12] {t("Deploy - …")} » en début de ligne, dans la f-string du menu.
|
|
|
|
|
|
RE_SHOWN = re.compile(r'^\[(\d+)\] \{t\("([^"]+)"\)\}', re.M)
|
|
|
|
|
|
# « elif status == "12": » suivi de « status = self.prompt_execute_deploy() »
|
|
|
|
|
|
RE_DISPATCH = re.compile(
|
|
|
|
|
|
r'elif status == "(\d+)":\s*\n\s*status = self\.(\w+)\(\)'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_execute_source():
|
|
|
|
|
|
"""Le corps de prompt_execute(), affichage et dispatch compris."""
|
|
|
|
|
|
source = TODO_PY.read_text(encoding="utf-8")
|
|
|
|
|
|
start = source.index("def prompt_execute(self):")
|
|
|
|
|
|
end = source.index("def prompt_install(self):", start)
|
|
|
|
|
|
return source[start:end]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExecuteMenuNumbering(unittest.TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.body = prompt_execute_source()
|
|
|
|
|
|
# [0] Retour est traité par le « if status == "0" » qui précède la
|
|
|
|
|
|
# chaîne d'elif : il s'affiche mais n'a pas de branche de dispatch.
|
|
|
|
|
|
self.shown = [
|
|
|
|
|
|
(int(num), label)
|
|
|
|
|
|
for num, label in RE_SHOWN.findall(self.body)
|
|
|
|
|
|
if num != "0"
|
|
|
|
|
|
]
|
|
|
|
|
|
self.dispatch = [
|
|
|
|
|
|
(int(num), method)
|
|
|
|
|
|
for num, method in RE_DISPATCH.findall(self.body)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_menu_was_actually_parsed(self):
|
|
|
|
|
|
# Si la forme du menu change, ce test doit tomber ici plutôt que de
|
|
|
|
|
|
# déclarer « tout va bien » sur une liste vide.
|
|
|
|
|
|
self.assertGreater(len(self.shown), 5)
|
|
|
|
|
|
self.assertEqual(len(self.shown), len(self.dispatch))
|
|
|
|
|
|
|
|
|
|
|
|
def test_zero_is_handled_before_the_elif_chain(self):
|
|
|
|
|
|
self.assertIn('if status == "0":', self.body)
|
|
|
|
|
|
|
|
|
|
|
|
def test_numbering_is_contiguous_from_one(self):
|
|
|
|
|
|
numbers = [num for num, _ in self.shown]
|
|
|
|
|
|
self.assertEqual(numbers, list(range(1, len(numbers) + 1)))
|
|
|
|
|
|
|
|
|
|
|
|
def test_every_shown_entry_has_the_matching_dispatch(self):
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
[num for num, _ in self.shown],
|
|
|
|
|
|
[num for num, _ in self.dispatch],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_dispatch_branch_is_unreachable(self):
|
|
|
|
|
|
shown = {num for num, _ in self.shown}
|
|
|
|
|
|
for num, method in self.dispatch:
|
|
|
|
|
|
self.assertIn(
|
|
|
|
|
|
num,
|
|
|
|
|
|
shown,
|
|
|
|
|
|
f"la branche [{num}] -> {method} n'est affichée nulle part",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Chaque entrée du menu et la méthode qu'elle DOIT atteindre, par le début
|
|
|
|
|
|
# de son libellé. Sans cette table, le test ne vérifie que l'alignement des
|
|
|
|
|
|
# numéros — et laisse passer le défaut même qu'une renumérotation produit :
|
|
|
|
|
|
# une entrée qui garde son rang mais atterrit dans le mauvais écran.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Une renumérotation, l'opération risquée, ne touche PAS cette table. Ajouter
|
|
|
|
|
|
# ou retirer une entrée demande d'y toucher, et c'est voulu : c'est le seul
|
|
|
|
|
|
# moment où quelqu'un doit dire où mène la nouvelle entrée.
|
|
|
|
|
|
EXPECTED = {
|
|
|
|
|
|
"Code": "prompt_execute_code",
|
|
|
|
|
|
"Config": "prompt_execute_config",
|
|
|
|
|
|
"Run": "prompt_execute_instance",
|
|
|
|
|
|
"Test": "prompt_execute_test",
|
|
|
|
|
|
"Process": "prompt_execute_process",
|
|
|
|
|
|
"Database": "prompt_execute_database",
|
|
|
|
|
|
"Analyse": "prompt_execute_analyse",
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
"Transform data": "prompt_execute_transform",
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
"Git": "prompt_execute_git",
|
|
|
|
|
|
"Doc": "prompt_execute_doc",
|
|
|
|
|
|
"GPT code": "prompt_execute_gpt_code",
|
|
|
|
|
|
"Automation": "prompt_execute_function",
|
|
|
|
|
|
"Deploy": "prompt_execute_deploy",
|
|
|
|
|
|
"Network": "prompt_execute_network",
|
|
|
|
|
|
"Security": "prompt_execute_security",
|
|
|
|
|
|
"Language": "_change_language",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _entry_key(self, label):
|
|
|
|
|
|
"""« Doc - Documentation search » -> « Doc »."""
|
|
|
|
|
|
return label.split(" - ", 1)[0].strip()
|
|
|
|
|
|
|
|
|
|
|
|
def test_every_entry_reaches_the_method_it_names(self):
|
|
|
|
|
|
dct_dispatch = dict(self.dispatch)
|
|
|
|
|
|
for num, label in self.shown:
|
|
|
|
|
|
key = self._entry_key(label)
|
|
|
|
|
|
self.assertIn(
|
|
|
|
|
|
key,
|
|
|
|
|
|
self.EXPECTED,
|
|
|
|
|
|
f"entrée [{num}] « {label} » absente de EXPECTED :"
|
|
|
|
|
|
" déclarez où elle mène",
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
dct_dispatch.get(num),
|
|
|
|
|
|
self.EXPECTED[key],
|
|
|
|
|
|
f"[{num}] « {label} » mène à"
|
|
|
|
|
|
f" {dct_dispatch.get(num)} au lieu de {self.EXPECTED[key]}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_expected_table_has_no_stale_entry(self):
|
|
|
|
|
|
# Une entrée retirée du menu doit sortir d'EXPECTED, sinon la table
|
|
|
|
|
|
# devient un cimetière qui ne protège plus rien.
|
|
|
|
|
|
shown_keys = {self._entry_key(label) for _, label in self.shown}
|
|
|
|
|
|
self.assertEqual(set(self.EXPECTED) - shown_keys, set())
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
|
|
|
|
|
|
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
class MenuCoherence:
|
|
|
|
|
|
"""Socle : un menu écrit en liste de dictionnaires est-il cohérent ?
|
|
|
|
|
|
|
|
|
|
|
|
Ce piège-là ne dépend pas du menu : seules les entrées
|
|
|
|
|
|
« prompt_description » consomment un numéro (les « section » sont des
|
|
|
|
|
|
titres), et le dispatch les renumérote à la main. Insérer une entrée avant
|
|
|
|
|
|
la dernière décale tout ce qui suit sans que rien ne proteste — c'est
|
|
|
|
|
|
arrivé en ajoutant l'émulateur Android avant « List available images ».
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
Depuis que les menus vivent dans leurs propres fichiers (le refactor de
|
|
|
|
|
|
todo.py), ce socle sert DEUX menus : QEMU/KVM et Proxmox. Un troisième
|
|
|
|
|
|
n'aura qu'à déclarer ses quatre attributs.
|
|
|
|
|
|
|
2026-09-02 04:43:25 -04:00
|
|
|
|
Une entrée peut aussi porter sa destination dans « method » plutôt que
|
|
|
|
|
|
dans un « elif status » numéroté. Elle échappe alors à la renumérotation
|
|
|
|
|
|
par construction, et EXPECTED la vérifie contre cette clé.
|
|
|
|
|
|
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
À déclarer par la sous-classe : SOURCE (le fichier), ENTRY (la ligne
|
|
|
|
|
|
« def prompt_execute_… »), END (le membre suivant, qui borne la lecture) et
|
|
|
|
|
|
EXPECTED (où mène chaque entrée, par le début de son libellé).
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
"""
|
|
|
|
|
|
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
SOURCE = None
|
|
|
|
|
|
ENTRY = ""
|
|
|
|
|
|
END = ""
|
|
|
|
|
|
EXPECTED = {}
|
|
|
|
|
|
MINIMUM = 10
|
|
|
|
|
|
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
RE_ENTRY = re.compile(
|
|
|
|
|
|
r'"(section|prompt_description)": t\(\s*\n?\s*"([^"]+)"'
|
|
|
|
|
|
)
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
# Les lignes de COMMENTAIRE entre le « elif » et l'appel sont sautées :
|
|
|
|
|
|
# une entrée expliquée devenait invisible pour ce test, qui annonçait alors
|
|
|
|
|
|
# « 18 affichées, 17 dispatchées » sans qu'aucune entrée ne manque. Un test
|
|
|
|
|
|
# ne doit pas dépendre de l'endroit où quelqu'un met un commentaire.
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
RE_DISPATCH_CALL = re.compile(
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
r'(?:el)?if status == "(\d+)":\s*\n(?:\s*#.*\n)*'
|
[FIX] todo : rendre au découpage ses colonnes de télémétrie
« Il manque plein d'informations qu'il y avait avant » : l'écran de
télémétrie construit son arbre en LISANT le code — un fichier, sa première
classe. Depuis que les menus QEMU/KVM et Proxmox vivent dans des mixins,
leurs colonnes avaient disparu de cet écran ; les commandes s'exécutaient
toujours, mais on ne pouvait plus les lancer de là. L'arbre lit maintenant
aussi les mixins, trouvés dans les imports de todo.py — un mixin ajouté
demain apparaîtra sans qu'on y pense.
Le menu Proxmox n'avait pas d'étiquette : son fil d'Ariane s'arrêtait deux
niveaux plus haut, sur « Deploy ». Vérifié : 16 commandes QEMU/KVM et 18
Proxmox dans l'arbre, contre zéro et zéro.
--- EN ---
"A lot of information that used to be there is missing": the telemetry screen
builds its tree by READING the code — one file, its first class. Since the
QEMU/KVM and Proxmox menus moved into mixins, their columns had vanished from
that screen; the commands still ran, but could no longer be launched from
there. The tree now reads the mixins too, found in todo.py's own imports — a
mixin added tomorrow shows up without anyone thinking about it.
The Proxmox menu had no label: its breadcrumb stopped two levels up, at
"Deploy". Verified: 16 QEMU/KVM commands and 18 Proxmox ones in the tree,
against zero and zero.
Assisted-by: Claude Opus 5
2026-08-24 04:17:56 -04:00
|
|
|
|
r"\s*(?:status = )?self\.(\w+)\("
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
)
|
2026-09-02 04:43:25 -04:00
|
|
|
|
# Une entrée qui porte sa destination dans « method » se dispatche seule,
|
|
|
|
|
|
# par le repli générique. Elle n'a pas de numéro dans le code, donc aucune
|
|
|
|
|
|
# renumérotation ne peut la désaligner : c'est le seul moyen de placer une
|
|
|
|
|
|
# entrée codée en dur APRÈS des entrées venues de la configuration, dont
|
|
|
|
|
|
# le nombre n'est pas connu à la lecture du source.
|
|
|
|
|
|
RE_SELF_DISPATCH = re.compile(
|
|
|
|
|
|
r'"prompt_description": t\(\s*\n?\s*"([^"]+)"\s*\)?,?\s*\n'
|
|
|
|
|
|
r'\s*"method": "(\w+)"'
|
|
|
|
|
|
)
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
source = self.SOURCE.read_text(encoding="utf-8")
|
|
|
|
|
|
start = source.index(self.ENTRY)
|
|
|
|
|
|
end = source.index(self.END, start)
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
self.body = source[start:end]
|
2026-09-02 04:43:25 -04:00
|
|
|
|
self.self_dispatch = dict(self.RE_SELF_DISPATCH.findall(self.body))
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
num = 0
|
|
|
|
|
|
self.shown = []
|
|
|
|
|
|
for kind, label in self.RE_ENTRY.findall(self.body):
|
|
|
|
|
|
if kind == "prompt_description":
|
|
|
|
|
|
num += 1
|
|
|
|
|
|
self.shown.append((num, label))
|
2026-09-02 04:43:25 -04:00
|
|
|
|
self.numbered = [
|
|
|
|
|
|
(n, label)
|
|
|
|
|
|
for n, label in self.shown
|
|
|
|
|
|
if label not in self.self_dispatch
|
|
|
|
|
|
]
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
self.dispatch = [
|
|
|
|
|
|
(int(n), m) for n, m in self.RE_DISPATCH_CALL.findall(self.body)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_menu_was_actually_parsed(self):
|
|
|
|
|
|
"""Sur une liste vide, tout test passe : mieux vaut tomber ici."""
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
self.assertGreater(len(self.shown), self.MINIMUM)
|
2026-09-02 04:43:25 -04:00
|
|
|
|
self.assertEqual(len(self.numbered), len(self.dispatch))
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
|
|
|
|
|
|
def test_numbering_is_contiguous_from_one(self):
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
[n for n, _ in self.shown],
|
|
|
|
|
|
list(range(1, len(self.shown) + 1)),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_every_shown_entry_has_the_matching_dispatch(self):
|
|
|
|
|
|
self.assertEqual(
|
2026-09-02 04:43:25 -04:00
|
|
|
|
[n for n, _ in self.numbered], [n for n, _ in self.dispatch]
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _key(self, label):
|
|
|
|
|
|
for key in self.EXPECTED:
|
|
|
|
|
|
if label.startswith(key):
|
|
|
|
|
|
return key
|
|
|
|
|
|
return label
|
|
|
|
|
|
|
|
|
|
|
|
def test_every_entry_reaches_the_method_it_names(self):
|
|
|
|
|
|
dct = dict(self.dispatch)
|
|
|
|
|
|
for num, label in self.shown:
|
|
|
|
|
|
key = self._key(label)
|
|
|
|
|
|
self.assertIn(
|
|
|
|
|
|
key,
|
|
|
|
|
|
self.EXPECTED,
|
|
|
|
|
|
f"entrée [{num}] « {label} » absente d'EXPECTED :"
|
|
|
|
|
|
" déclarez où elle mène",
|
|
|
|
|
|
)
|
2026-09-02 04:43:25 -04:00
|
|
|
|
atteint = self.self_dispatch.get(label, dct.get(num))
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
self.assertEqual(
|
2026-09-02 04:43:25 -04:00
|
|
|
|
atteint,
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
self.EXPECTED[key],
|
2026-09-02 04:43:25 -04:00
|
|
|
|
f"[{num}] « {label} » mène à {atteint}"
|
[ADD] test: apparier l'affichage et le dispatch du menu QEMU
Le menu Execute avait ce garde-fou, le menu QEMU non — et c'est lui qui vient
de renumérer, l'émulateur Android s'insérant avant « List available images ».
Sa forme est pire à l'œil : une liste de dictionnaires où les « section » ne
consomment pas de numéro, donc le décalage ne se voit pas en lisant.
Le test relit les deux écritures et les apparie, puis une table dit où chaque
entrée doit mener. Inverser les deux derniers crans du dispatch le fait bien
tomber, en nommant l'entrée fautive.
--- EN ---
The Execute menu had this guard, the QEMU menu did not — and the QEMU one is
what just got renumbered, the Android emulator slotting in before "List
available images". Its shape is worse to eyeball: a list of dicts where
"section" entries consume no number, so a shift does not show when reading.
The test reads both spellings and pairs them, then a table states where each
entry must lead. Swapping the last two dispatch branches does make it fail,
naming the offending entry.
Assisted-by: Claude Opus 5
2026-08-19 03:39:20 -04:00
|
|
|
|
f" au lieu de {self.EXPECTED[key]}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_expected_table_has_no_stale_entry(self):
|
|
|
|
|
|
keys = {self._key(label) for _, label in self.shown}
|
|
|
|
|
|
self.assertEqual(set(self.EXPECTED) - keys, set())
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
|
2026-09-02 04:43:25 -04:00
|
|
|
|
def test_self_dispatched_entries_name_a_real_method(self):
|
|
|
|
|
|
"""« method » est une chaîne : rien ne la relie au code sans ceci."""
|
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
|
|
for label, method in self.self_dispatch.items():
|
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
|
hasattr(TODO, method),
|
|
|
|
|
|
f"« {label} » mène à {method}, qui n'existe pas",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
|
[ADD] proxmox : suivre une VM distante, changer son état, étendre le menu
Les trois manques restants, demandés. Les colonnes vivantes du suivi — écrit
par seconde, RAM, disque — venaient de virsh, qui ne connaît pas les VM d'un
hôte Proxmox : elles restaient vides, et le relevé d'état les déclarait même
« effacées », ce qui éteignait le reste. L'hôte sait tout cela en un appel
(« pvesh get /cluster/resources »), et le relevé prend la forme de celui de
virsh pour que rien en aval ne distingue la source. Un appel par hôte, mis en
cache cinq secondes : une poignée de main ssh coûte 1 s, virsh 0,03 s.
« Lister les VM » propose maintenant d'en changer l'état, comme QEMU/KVM —
éteindre proprement avant de couper le courant, l'ordre le dit. Et le menu
accepte les commandes ajoutées par todo.json. Enfin « models » manquait aux
traductions : le rapport de migration sortait « 812 models » en français.
--- EN ---
The three remaining gaps, as asked. The dashboard's live columns — written per
second, RAM, disk — came from virsh, which knows nothing of a Proxmox host's
VMs: they stayed empty, and the state probe even declared them "gone", which
switched off the rest. The host knows all of it in one call ("pvesh get
/cluster/resources"), and the reading takes virsh's own shape so nothing
downstream tells the sources apart. One call per host, cached five seconds: an
ssh handshake costs 1 s, virsh 0.03 s.
"List VMs" now offers to change their state, like QEMU/KVM — clean shutdown
before pulling the plug, the order says so. And the menu accepts the commands
todo.json adds. Finally "models" was missing from the translations: the
migration report printed "812 models" in French.
Assisted-by: Claude Opus 5
2026-08-24 05:11:58 -04:00
|
|
|
|
class TestLaParitéProxmox(unittest.TestCase):
|
|
|
|
|
|
"""Deux manques signalés par l'audit du découpage, comblés.
|
|
|
|
|
|
|
|
|
|
|
|
Le menu Proxmox n'offrait pas de changer l'état d'une VM (QEMU/KVM l'a
|
|
|
|
|
|
dans « Lister les VM »), et n'acceptait pas les commandes ajoutées par
|
|
|
|
|
|
todo.json — deux capacités que son vis-à-vis avait.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
|
cls.src = (TODO_DIR / "proxmox_menu.py").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_list_offers_to_change_the_state(self):
|
|
|
|
|
|
self.assertIn("_pve_change_state", self.src)
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
|
from script.todo.todo import TODO as CLASSE
|
|
|
|
|
|
|
|
|
|
|
|
self.assertTrue(callable(CLASSE._pve_change_state))
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_clean_shutdown_comes_before_pulling_the_plug(self):
|
|
|
|
|
|
# « shutdown » laisse Odoo fermer ses connexions PostgreSQL ; « stop »
|
|
|
|
|
|
# coupe le courant. L'ordre des choix est la seule chose qui le dit.
|
|
|
|
|
|
self.assertLess(
|
|
|
|
|
|
self.src.index("shutdown (clean)"),
|
|
|
|
|
|
self.src.index("stop (pulls the plug)"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_menu_reads_its_extra_commands_from_todo_json(self):
|
|
|
|
|
|
self.assertIn('get_config("proxmox_from_makefile")', self.src)
|
|
|
|
|
|
# Et le dispatch sait les lancer, sections non comptées.
|
|
|
|
|
|
self.assertIn("execute_from_configuration", self.src)
|
|
|
|
|
|
|
|
|
|
|
|
|
[IMP] menu proxmox : une icône à chaque entrée qui n'en portait pas
Ten of the eighteen entries carried no pictogram, and the eye picks one out
of such a list before it reads. Each takes the icon its siblings already use
elsewhere in the tool — 📋 lists, 🧹 cleans, 📊 measures, 🔍 previews, 📥
downloads, 🗂 catalogues, 🖥 tunnels a desktop — so the same picture keeps
meaning the same thing from one menu to the next. A table in the tests holds
the pairing, in both languages, and checks the menu still shows the entries
it freezes.
--- FR ---
Dix des dix-huit entrées ne portaient aucun pictogramme, et l'œil s'y repère
avant de lire. Chacune reçoit l'icône que ses semblables portent déjà
ailleurs dans l'outil — 📋 liste, 🧹 nettoie, 📊 mesure, 🔍 prévisualise, 📥
télécharge, 🗂 catalogue, 🖥 ouvre un bureau —, si bien qu'une même image
garde le même sens d'un menu à l'autre. Un tableau dans les tests fige le
couple, dans les deux langues, et vérifie que le menu affiche encore les
entrées qu'il fige.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
class TestLesIconesDuMenuProxmox(unittest.TestCase):
|
|
|
|
|
|
"""L'icône vit DANS la chaîne traduite, et les deux langues la portent.
|
|
|
|
|
|
|
|
|
|
|
|
Une entrée sans icône se perd dans une liste de dix-huit : l'œil s'y
|
|
|
|
|
|
repère par le pictogramme avant de lire. Le même pictogramme dit la même
|
|
|
|
|
|
chose partout dans l'outil — 📋 liste, 🧹 nettoie, 📊 mesure — et ce
|
|
|
|
|
|
tableau est ce qui l'empêche de dériver d'un menu à l'autre.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
ICONES = {
|
|
|
|
|
|
"Deploy a VM on the Proxmox host": "🚀",
|
|
|
|
|
|
"Preview a deployment (dry-run, nothing sent)": "🔍",
|
|
|
|
|
|
"Download a cloud image on the host": "📥",
|
|
|
|
|
|
"Reopen install monitoring (last run / history)": "📈",
|
|
|
|
|
|
"List VMs (qm list)": "📋",
|
|
|
|
|
|
"Show a VM IP address": "🌐",
|
|
|
|
|
|
"Open the console on a VM": "🖥",
|
|
|
|
|
|
"Resize a VM disk": "📐",
|
|
|
|
|
|
"Delete VM(s)": "🗑",
|
|
|
|
|
|
"Clean up (orphan disks)": "🧹",
|
|
|
|
|
|
"Test a VM (open Odoo in a CLI browser)": "🧪",
|
|
|
|
|
|
"Statistics (host and VMs)": "📊",
|
|
|
|
|
|
"SSH configuration (~/.ssh/config, ProxyJump)": "🔑",
|
|
|
|
|
|
"Remote desktop tunnel (VNC/RDP over SSH)": "🖥",
|
|
|
|
|
|
"Android emulator (start, tunnel, scrcpy)": "📱",
|
|
|
|
|
|
"List available images and their specs": "🗂",
|
|
|
|
|
|
"Proxmox - example sequence (dry-run)": "🎬",
|
|
|
|
|
|
"Change the Proxmox host": "🔀",
|
|
|
|
|
|
"Host": "🏠",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def test_chaque_entree_porte_son_icone(self):
|
|
|
|
|
|
from script.todo import todo_i18n
|
|
|
|
|
|
|
|
|
|
|
|
for cle, icone in self.ICONES.items():
|
|
|
|
|
|
entree = todo_i18n.TRANSLATIONS[cle]
|
|
|
|
|
|
for langue in ("fr", "en"):
|
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
|
entree[langue].startswith(icone),
|
|
|
|
|
|
f"« {cle} » ({langue}) ne commence pas par {icone} :"
|
|
|
|
|
|
f" {entree[langue]}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_le_menu_affiche_bien_ces_entrees(self):
|
|
|
|
|
|
"""Le tableau ci-dessus ne vaut que s'il décrit le menu RÉEL : une
|
|
|
|
|
|
entrée renommée le laisserait figer une icône que personne ne voit."""
|
|
|
|
|
|
src = (TODO_DIR / "proxmox_menu.py").read_text(encoding="utf-8")
|
|
|
|
|
|
for cle in self.ICONES:
|
|
|
|
|
|
# La chaîne SEULE : une entrée longue s'écrit « t( » sur une
|
|
|
|
|
|
# ligne et sa chaîne sur la suivante, et chercher l'appel entier
|
|
|
|
|
|
# ne trouverait que les courtes.
|
|
|
|
|
|
self.assertIn(
|
|
|
|
|
|
f'"{cle}"', src, f"« {cle} » n'est plus dans le menu"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
[FIX] todo : rendre au découpage ses colonnes de télémétrie
« Il manque plein d'informations qu'il y avait avant » : l'écran de
télémétrie construit son arbre en LISANT le code — un fichier, sa première
classe. Depuis que les menus QEMU/KVM et Proxmox vivent dans des mixins,
leurs colonnes avaient disparu de cet écran ; les commandes s'exécutaient
toujours, mais on ne pouvait plus les lancer de là. L'arbre lit maintenant
aussi les mixins, trouvés dans les imports de todo.py — un mixin ajouté
demain apparaîtra sans qu'on y pense.
Le menu Proxmox n'avait pas d'étiquette : son fil d'Ariane s'arrêtait deux
niveaux plus haut, sur « Deploy ». Vérifié : 16 commandes QEMU/KVM et 18
Proxmox dans l'arbre, contre zéro et zéro.
--- EN ---
"A lot of information that used to be there is missing": the telemetry screen
builds its tree by READING the code — one file, its first class. Since the
QEMU/KVM and Proxmox menus moved into mixins, their columns had vanished from
that screen; the commands still ran, but could no longer be launched from
there. The tree now reads the mixins too, found in todo.py's own imports — a
mixin added tomorrow shows up without anyone thinking about it.
The Proxmox menu had no label: its breadcrumb stopped two levels up, at
"Deploy". Verified: 16 QEMU/KVM commands and 18 Proxmox ones in the tree,
against zero and zero.
Assisted-by: Claude Opus 5
2026-08-24 04:17:56 -04:00
|
|
|
|
class TestLArbreDesMenus(unittest.TestCase):
|
|
|
|
|
|
"""L'écran de télémétrie lit le CODE, pas la classe assemblée.
|
|
|
|
|
|
|
|
|
|
|
|
`build_code_tree()` parse un fichier et n'y prend que la première classe.
|
|
|
|
|
|
Depuis le découpage, les menus QEMU/KVM et Proxmox vivent dans des mixins :
|
|
|
|
|
|
leur colonne avait disparu de cet écran — les commandes s'exécutaient
|
|
|
|
|
|
toujours, mais on ne pouvait plus les lancer de là ni les lire. C'est ce
|
|
|
|
|
|
que « il manque plein d'informations qu'il y avait avant » désignait.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
|
from script.todo.todo_telemetry import build_code_tree
|
|
|
|
|
|
|
|
|
|
|
|
cls.arbre = build_code_tree()
|
|
|
|
|
|
|
|
|
|
|
|
def _noeud(self, libelle, noeud=None):
|
|
|
|
|
|
noeud = noeud if noeud is not None else self.arbre
|
|
|
|
|
|
if noeud.get("label") == libelle:
|
|
|
|
|
|
return noeud
|
|
|
|
|
|
for enfant in noeud.get("children") or []:
|
|
|
|
|
|
trouve = self._noeud(libelle, enfant)
|
|
|
|
|
|
if trouve:
|
|
|
|
|
|
return trouve
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_tree_is_built_at_all(self):
|
|
|
|
|
|
self.assertIsNotNone(self.arbre)
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_mixin_files_come_from_the_imports(self):
|
|
|
|
|
|
# Lus dans les imports de todo.py : un mixin ajouté demain apparaît
|
|
|
|
|
|
# sans qu'on pense à l'inscrire ici.
|
|
|
|
|
|
from script.todo.todo_telemetry import _mixin_files
|
|
|
|
|
|
|
|
|
|
|
|
noms = {f.name for f in _mixin_files(TODO_DIR / "todo.py")}
|
|
|
|
|
|
self.assertIn("qemu_menu.py", noms)
|
|
|
|
|
|
self.assertIn("proxmox_menu.py", noms)
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_qemu_column_carries_its_commands(self):
|
|
|
|
|
|
noeud = self._noeud("QEMU/KVM")
|
|
|
|
|
|
self.assertIsNotNone(noeud, "colonne QEMU/KVM absente de l'arbre")
|
|
|
|
|
|
self.assertGreaterEqual(len(noeud.get("children") or []), 15)
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_proxmox_column_too(self):
|
|
|
|
|
|
noeud = self._noeud("Proxmox VE")
|
|
|
|
|
|
self.assertIsNotNone(noeud, "colonne Proxmox VE absente de l'arbre")
|
|
|
|
|
|
self.assertGreaterEqual(len(noeud.get("children") or []), 15)
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_breadcrumb_names_the_proxmox_menu(self):
|
|
|
|
|
|
# Sans étiquette, le fil d'Ariane sautait le menu Proxmox : on lisait
|
|
|
|
|
|
# « TODO › Execute › Deploy » en étant deux niveaux plus bas.
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
|
from script.todo.todo import TODO as CLASSE
|
|
|
|
|
|
|
|
|
|
|
|
self.assertIn("prompt_execute_proxmox", CLASSE._MENU_LABELS)
|
|
|
|
|
|
|
|
|
|
|
|
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
class TestQemuMenuNumbering(MenuCoherence, unittest.TestCase):
|
|
|
|
|
|
"""Le menu QEMU/KVM, désormais dans script/todo/qemu_menu.py."""
|
|
|
|
|
|
|
|
|
|
|
|
SOURCE = TODO_DIR / "qemu_menu.py"
|
|
|
|
|
|
ENTRY = "def prompt_execute_qemu(self):"
|
|
|
|
|
|
END = "def _qemu_stats(self):"
|
|
|
|
|
|
|
|
|
|
|
|
# Où mène chaque entrée, par le début de son libellé. Une renumérotation ne
|
|
|
|
|
|
# touche PAS cette table ; ajouter une entrée l'exige, et c'est le seul
|
|
|
|
|
|
# moment où quelqu'un doit dire où elle mène.
|
|
|
|
|
|
EXPECTED = {
|
|
|
|
|
|
"Deploy VM(s)": "_qemu_deploy",
|
|
|
|
|
|
"Preview a deployment": "_qemu_deploy",
|
|
|
|
|
|
"Download a cloud image only": "_qemu_download_image",
|
|
|
|
|
|
"Reopen": "_qemu_reopen_monitor",
|
|
|
|
|
|
"List VMs": "_qemu_list_vms",
|
|
|
|
|
|
"Show a VM IP address": "_qemu_show_ip",
|
|
|
|
|
|
"Open the console on a VM": "_qemu_console",
|
|
|
|
|
|
"Resize a VM disk": "_qemu_resize_disk",
|
|
|
|
|
|
"Delete VM(s)": "_qemu_delete_vm",
|
|
|
|
|
|
"Clean up QEMU": "_qemu_cleanup",
|
|
|
|
|
|
"Test": "_qemu_test_vm",
|
|
|
|
|
|
"Statistics": "_qemu_stats",
|
|
|
|
|
|
"SSH configuration": "_qemu_ssh_config_menu",
|
|
|
|
|
|
"Remote desktop tunnel": "_qemu_tunnel_menu",
|
|
|
|
|
|
"Android emulator": "_qemu_emulator_menu",
|
|
|
|
|
|
"List available images": "_qemu_list_images",
|
2026-09-03 04:11:36 -04:00
|
|
|
|
"Recover files from a VM disk (libguestfs)": "_qemu_recover_files",
|
|
|
|
|
|
"Diagnostics (report to share)": "_qemu_diagnostics",
|
[ADD] qemu réseau : voir et recréer le sous-réseau des VM
Une VM prend son adresse dans le /24 de son réseau et en sort par le .1,
porté par le pont : déplacer ce /24 sous une VM allumée lui laisse un bail
qui ne mène nulle part, et abattre le réseau détache son tap, que libvirt
n'y remet pas. network_qemu.py tient les trois gestes dans l'ordre — arrêter
les VM attachées, redéfinir le réseau sur le préfixe voulu, rallumer celles
qu'il a arrêtées, elles seules. Une VM sourde au shutdown annule la
redéfinition plutôt que d'y perdre son pont ; --force-off coupe le courant.
Un préfixe qui recouvre ce que l'hôte route déjà est refusé.
Vérifié : 13 tests neufs, la numérotation du menu, une passe à blanc.
--- EN ---
A VM takes its address in its network's /24 and leaves through the .1,
carried by the bridge: moving that /24 under a live VM leaves it a lease
leading nowhere, and tearing the network down detaches its tap, which
libvirt does not put back. network_qemu.py holds the three steps in order —
shut down the attached VMs, redefine the network on the wanted prefix, start
again the ones it stopped, those only. A VM deaf to the shutdown cancels the
redefinition rather than losing its bridge; --force-off cuts the power.
A prefix overlapping what the host already routes is refused.
Checked: 13 new tests, the menu numbering, one dry run.
Assisted-by: Claude Opus 5
2026-09-04 03:31:22 -04:00
|
|
|
|
"Show the libvirt network state": "_qemu_network_status",
|
|
|
|
|
|
"Recreate the VM subnet": "_qemu_network_recreate",
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
class TestAnalyseMenuNumbering(MenuCoherence, unittest.TestCase):
|
|
|
|
|
|
"""Le menu Analyse, qui n'avait aucun garde.
|
|
|
|
|
|
|
|
|
|
|
|
Il en a pourtant besoin plus que les autres : ses entrées sont
|
|
|
|
|
|
regroupées en cinq sections, et une section ne consomme pas de numéro.
|
|
|
|
|
|
Ajouter « Instance » avant la dernière entrée décalait tout ce qui
|
|
|
|
|
|
suivait sans que rien ne proteste.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
SOURCE = TODO_DIR / "todo.py"
|
|
|
|
|
|
ENTRY = "def prompt_execute_analyse(self):"
|
|
|
|
|
|
END = "def execute_analyse_module_package(self):"
|
|
|
|
|
|
MINIMUM = 5
|
|
|
|
|
|
|
|
|
|
|
|
EXPECTED = {
|
|
|
|
|
|
"Tables and database size": "execute_analyse_schema_size",
|
|
|
|
|
|
"Customised views": "execute_analyse_view_custom",
|
|
|
|
|
|
"Studio and hand-made": "execute_analyse_custom_field",
|
|
|
|
|
|
"Quality of a migration": "execute_analyse_migration_quality",
|
|
|
|
|
|
"Modules missing": "execute_analyse_module_package",
|
|
|
|
|
|
"Dependencies between": "execute_analyse_module_dependency",
|
|
|
|
|
|
"Attachment files missing": "execute_analyse_filestore",
|
|
|
|
|
|
"Monitoring - a backup": "execute_analyse_monitoring",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
|
class TestDatabaseMenuNumbering(MenuCoherence, unittest.TestCase):
|
|
|
|
|
|
"""Le menu Database, qui manie des bases entières.
|
|
|
|
|
|
|
|
|
|
|
|
Il n'avait aucun garde, et c'est celui où une renumérotation coûte le
|
|
|
|
|
|
plus cher : sa dernière entrée EFFACE une base. Insérer « Dupliquer »
|
|
|
|
|
|
avant elle décale « Effacer » de [4] à [5] — si le dispatch ne suit
|
|
|
|
|
|
pas, taper [4] efface au lieu de copier.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
SOURCE = TODO_DIR / "todo.py"
|
|
|
|
|
|
ENTRY = "def prompt_execute_database(self):"
|
|
|
|
|
|
END = "def prompt_execute_analyse(self):"
|
|
|
|
|
|
MINIMUM = 4
|
|
|
|
|
|
|
|
|
|
|
|
# Ce menu délègue à `self.db_manager.methode()`, pas à `self.methode()`.
|
|
|
|
|
|
# Le motif du socle ne voit que la forme courte : sans cette surcharge
|
|
|
|
|
|
# il lit ZÉRO dispatch et ne compare plus rien — un garde qui passe au
|
|
|
|
|
|
# vert sans rien garder.
|
|
|
|
|
|
RE_DISPATCH_CALL = re.compile(
|
|
|
|
|
|
r'(?:el)?if status == "(\d+)":\s*\n(?:\s*#.*\n)*'
|
|
|
|
|
|
r"\s*(?:status = )?self\.(?:\w+\.)*(\w+)\("
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
EXPECTED = {
|
|
|
|
|
|
"Create backup": "create_backup_from_database",
|
|
|
|
|
|
"Download database": "download_database_backup_cli",
|
|
|
|
|
|
"Restore from backup": "restore_from_database",
|
|
|
|
|
|
"Duplicate a database": "duplicate_database",
|
|
|
|
|
|
"Erase a database": "drop_database",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
class TestProxmoxMenuNumbering(MenuCoherence, unittest.TestCase):
|
|
|
|
|
|
"""Le menu Proxmox : dix-huit entrées, le même piège.
|
|
|
|
|
|
|
|
|
|
|
|
Quatre d'entre elles mènent VOLONTAIREMENT à des méthodes du menu QEMU —
|
|
|
|
|
|
c'est le même travail, et le refactor n'a pas dupliqué ce code. La table
|
|
|
|
|
|
le dit noir sur blanc : si quelqu'un les recopiait un jour, ce test
|
|
|
|
|
|
montrerait que la cible a changé.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
SOURCE = TODO_DIR / "proxmox_menu.py"
|
|
|
|
|
|
ENTRY = "def prompt_execute_proxmox(self):"
|
|
|
|
|
|
END = "def _pve_fetch_image(self):"
|
|
|
|
|
|
MINIMUM = 15
|
|
|
|
|
|
|
|
|
|
|
|
EXPECTED = {
|
|
|
|
|
|
"Deploy a VM on the Proxmox host": "_pve_deploy",
|
|
|
|
|
|
"Preview a deployment": "_pve_deploy",
|
|
|
|
|
|
"Download a cloud image on the host": "_pve_fetch_image",
|
|
|
|
|
|
"Reopen": "_qemu_reopen_monitor",
|
|
|
|
|
|
"List VMs (qm list)": "_pve_list",
|
|
|
|
|
|
"Show a VM IP address": "_pve_vm_ip",
|
|
|
|
|
|
"Open the console on a VM": "_pve_console",
|
|
|
|
|
|
"Resize a VM disk": "_pve_resize",
|
|
|
|
|
|
"Delete VM(s)": "_pve_delete",
|
|
|
|
|
|
"Clean up (orphan disks)": "_pve_cleanup",
|
|
|
|
|
|
"Test a VM": "_pve_test_vm",
|
|
|
|
|
|
"Statistics (host and VMs)": "_pve_stats",
|
|
|
|
|
|
"SSH configuration": "_pve_ssh_config",
|
|
|
|
|
|
"Remote desktop tunnel": "_qemu_tunnel_menu",
|
|
|
|
|
|
"Android emulator": "_qemu_emulator_menu",
|
|
|
|
|
|
"List available images": "_qemu_list_images",
|
|
|
|
|
|
"Proxmox - example sequence": "_pve_example",
|
|
|
|
|
|
"Change the Proxmox host": "_pve_forget_host",
|
2026-08-31 06:17:48 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestGitMenuNumbering(MenuCoherence, unittest.TestCase):
|
|
|
|
|
|
"""Le menu Git, le seul dont todo.json suit des entrées codées en dur.
|
|
|
|
|
|
|
2026-09-02 04:01:10 -04:00
|
|
|
|
Ses premières entrées sont écrites à la main, les suivantes viennent de
|
|
|
|
|
|
`git_from_makefile` et le repli générique les renumérote tout seul :
|
|
|
|
|
|
ajouter une entrée codée en dur pousse celles de todo.json d'un rang sans
|
|
|
|
|
|
que rien ne le dise. Une entrée codée en dur oubliée dans le dispatch
|
|
|
|
|
|
ferait lancer la commande du voisin sous le libellé attendu.
|
2026-08-31 06:17:48 -04:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
SOURCE = TODO_DIR / "todo.py"
|
|
|
|
|
|
ENTRY = "def prompt_execute_git(self):"
|
|
|
|
|
|
END = "def _git_install_hooks(self):"
|
|
|
|
|
|
MINIMUM = 2
|
|
|
|
|
|
|
|
|
|
|
|
EXPECTED = {
|
|
|
|
|
|
"Local git server": "prompt_execute_git_local_server",
|
|
|
|
|
|
"Add a remote to a local repository": "_git_add_remote",
|
|
|
|
|
|
"Install git hooks": "_git_install_hooks",
|
2026-09-02 04:01:10 -04:00
|
|
|
|
"Set merge.conflictStyle": "_git_set_conflict_style",
|
2026-09-02 04:43:25 -04:00
|
|
|
|
"Install Starship on Shell": "_shell_install_starship",
|
[ADD] script todo : installer Claude Code et opencode, PATH garanti
Ces installateurs posent leur binaire dans un répertoire du HOME que le PATH
d'un shell ne porte pas toujours : sans la ligne d'export, le binaire est là
et la commande reste introuvable. Le répertoire diffère d'un outil à l'autre,
et l'un des deux écrit déjà sa propre ligne — la présence se teste donc sur
le répertoire, pas sur la graphie, et rien n'est ajouté deux fois.
Le PATH d'un processus est figé depuis son démarrage : un nouveau shell est
nécessaire, ce que la sortie dit. Troisième écrivain dans le fichier de
shell, starship compris, d'où l'écriture mise en commun. Vérifié : 31 tests.
--- EN ---
These installers put their binary in a HOME directory that a shell's PATH
does not always carry: without the export line, the binary is there and the
command stays not found. The directory differs from one tool to the other,
and one of the two already writes its own line — presence is therefore tested
on the directory, not on the spelling, and nothing is added twice.
A process PATH is frozen since its start: a new shell is needed, which the
output says. Third writer into the shell file, starship included, hence the
shared write. Checked: 31 tests.
Assisted-by: Claude Opus 5
2026-09-02 04:51:03 -04:00
|
|
|
|
"Install Claude Code": "_shell_install_claude_code",
|
|
|
|
|
|
"Install opencode": "_shell_install_opencode",
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
class TestMenuLabels(unittest.TestCase):
|
|
|
|
|
|
"""Toute méthode de menu doit avoir son étiquette de fil d'Ariane.
|
|
|
|
|
|
|
|
|
|
|
|
Sans elle, `_menu_header` n'affiche pas le segment et
|
|
|
|
|
|
`todo_telemetry.build_code_tree` traite le menu comme une COMMANDE :
|
[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
2026-09-09 04:42:39 -04:00
|
|
|
|
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.
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
"""
|
|
|
|
|
|
|
[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
2026-09-09 04:42:39 -04:00
|
|
|
|
ECRANS_EXEMPTES = {
|
|
|
|
|
|
"_analyse_follow_up",
|
|
|
|
|
|
"rtk_install",
|
|
|
|
|
|
"generate_config_from_preconfiguration",
|
|
|
|
|
|
"debug_ide",
|
|
|
|
|
|
"execute_odoo_upgrade",
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
source = TODO_PY.read_text(encoding="utf-8")
|
|
|
|
|
|
tree = ast.parse(source)
|
|
|
|
|
|
cls = next(n for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
|
|
|
|
|
|
self.labels = set()
|
|
|
|
|
|
for node in cls.body:
|
|
|
|
|
|
if not isinstance(node, ast.Assign):
|
|
|
|
|
|
continue
|
|
|
|
|
|
if not any(
|
|
|
|
|
|
isinstance(tg, ast.Name) and tg.id == "_MENU_LABELS"
|
|
|
|
|
|
for tg in node.targets
|
|
|
|
|
|
):
|
|
|
|
|
|
continue
|
|
|
|
|
|
self.labels = {
|
|
|
|
|
|
key.value
|
|
|
|
|
|
for key in node.value.keys
|
|
|
|
|
|
if isinstance(key, ast.Constant)
|
|
|
|
|
|
}
|
|
|
|
|
|
self.dispatched = {
|
|
|
|
|
|
method
|
|
|
|
|
|
for _, method in RE_DISPATCH.findall(prompt_execute_source())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def test_menu_labels_was_parsed(self):
|
|
|
|
|
|
self.assertIn("prompt_execute", self.labels)
|
|
|
|
|
|
|
|
|
|
|
|
def test_analyse_menu_has_a_breadcrumb_label(self):
|
|
|
|
|
|
self.assertIn("prompt_execute_analyse", self.labels)
|
|
|
|
|
|
|
[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
2026-09-09 04:42:39 -04:00
|
|
|
|
@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)
|
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
def test_no_new_menu_forgets_its_label(self):
|
[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
2026-09-09 04:42:39 -04:00
|
|
|
|
missing = self._menus_du_paquet() - self.labels - self.ECRANS_EXEMPTES
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
self.assertEqual(
|
|
|
|
|
|
missing,
|
|
|
|
|
|
set(),
|
|
|
|
|
|
f"menus sans étiquette dans _MENU_LABELS : {sorted(missing)}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[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
2026-09-09 04:42:39 -04:00
|
|
|
|
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())
|
|
|
|
|
|
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
unittest.main()
|