refactor(logging): le journal parle une seule langue, et c'est l'anglais (0.1.83) - #192
Merged
Conversation
…rt (0.1.75) Le contrôle du pool rendait ok quand sa sonde échouait : ne pas savoir y valait « tout va bien ». Aggravant, la sonde invoquait virsh en direct, sans la détection de préfixe sudo -n que infra/libvirt.py a précisément construite pour ce cas. Sur une machine où l'URI système exige des droits, elle échouait donc toujours, et le contrôle était toujours vert. L'hypothèse de l'issue sur _check_kvm est confirmée : il sondait virsh version sans --connect, alors que libvirt.py documente que virsh sans --connect peut viser l'URI session selon la distribution. Un utilisateur hors du groupe libvirt avait donc KVM au vert pendant que provision, qui vise l'URI système, mourait sur « Pool not found ». Les deux sondes passent désormais par run_virsh, qui porte le --connect et la détection du préfixe. Une sonde impossible rend maintenant unknown, le jeton livré en 0.1.73 dont la docstring citait déjà ce pool comme le défaut à corriger. Cas ajouté : première sonde réussie mais seconde impossible vaut unknown aussi, parce que proposer « créer » ou « démarrer » au hasard ferait échouer la remédiation. Trois sudo virsh sans -n passent par run_virsh. Le -n n'est pas un détail : la sortie est capturée, donc un prompt sudo n'a pas de terminal où s'afficher, et l'appel pend. Un bail DHCP refusé remonte désormais à l'écran plutôt que dans un journal que personne ne lit — c'est ce qui faisait échouer un hôte en « injoignable » sans cause visible. Le garde-fou vise les sudo qui CAPTURENT leur sortie sans -n, et non tout sudo : le sudo -v de pré-authentification de doctor --fix est délibérément interactif, sa sortie n'est pas capturée, et son prompt a un terminal. C'est la capture sans -n qui produit la pendaison, pas sudo lui-même. Au passage, tout OSError de run_command devient un CommandError : un binaire qui disparaît ne fait plus planter un diagnostic. Vérifié : 778 tests dont 13 neufs, chacun éprouvé par mutation, 18 e2e, ruff, mypy strict, et doctor joué sur les deux dépôts fournisseurs. Closes #172 Closes #173 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e plus un workdir vide (0.1.76) ShellRuntime itère sur runtime.fixtures, pas sur le contenu de fixtures/. Les deux écarts possibles produisaient donc le même dégât, sans un mot : une fixture déclarée mais absente partait en logger.warning, une fixture présente mais non déclarée n'était jamais lue. Dans les deux cas run créait un challenge/work vide, sortait en 0, et l'apprenant n'avait rien à faire. C'est le défaut qui a rendu 7 labs de terraform-training injouables le 2026-07-28, tous marqués faits. Il se cachait d'autant mieux que les outils de vérification des corrigés copient, eux, le répertoire entier : la solution passait au vert pendant que le parcours apprenant était cassé. Mesuré avant de corriger : les 7 labs ont été réparés depuis, et les trois catalogues sont aujourd'hui indemnes. Le contrôle est donc préventif, et il ne casse aucune CI de catalogue tout en portant sur 70 labs shell. Deux niveaux, parce qu'ils servent deux moments. validate-structure prévient l'auteur en CI, dans les deux sens et sur le chemin qui s'évade ; le runtime protège l'apprenant qui joue un lab déjà publié. Les fichiers cachés sont exemptés : un .gitkeep versionne un répertoire vide, et le signaler serait un faux positif que chaque auteur apprendrait à ignorer — un contrôle qu'on apprend à ignorer ne contrôle plus rien. Renversement assumé, et il est motivé. Un test existant affirmait l'inverse : « a typo in one entry must not deprive the learner of the whole workdir ». C'est le fichier manquant qui l'en prive. Une erreur d'auteur n'est pas quelque chose que l'apprenant peut réparer, et un exercice amputé le fait échouer au check pour des raisons qu'il cherchera dans son propre travail. Le test est réécrit, pas supprimé, et il porte le motif du renversement. Ce que son intention demandait est gardé là où il fallait : le message nomme toutes les fixtures fautives d'un coup. La validation précède toute copie, donc c'est tout ou rien : un workdir à moitié rempli a l'air de marcher, ce qui est pire qu'un refus. Vérifié sur un lab réel copié dans un catalogue jetable : validate-structure nomme chaque fixture et sort en 1, run refuse et sort en 2, le workdir reste vide malgré 6 fixtures valides. 790 tests dont 12 neufs, chacun éprouvé par mutation dans les deux sens, 18 e2e, ruff, mypy strict, et les trois catalogues réels validés sans un faux positif. Closes #177 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sont déclarés (0.1.77)
run_command transformait TimeoutExpired, FileNotFoundError et toute autre
OSError en CommandError même avec check=False. Tout appelant qui croyait
recevoir un CommandResult en toutes circonstances se trompait, et le nom du
paramètre l'encourageait à le croire. Le docstring l'affirmait d'ailleurs :
« Raises: si check=True et le code de retour est != 0 », ce qui était faux.
Deux victimes, reproduites par exécution avant d'être corrigées. Sur un PATH
sans git, catalog add sortait en trace Python : c'est la deuxième commande du
parcours d'accueil, donc le plus mauvais moment pour montrer une trace. Et une
sonde de disponibilité qui expirait faisait sauter la boucle de réessai
entière, au lieu d'être comptée comme un échec à réessayer — or c'est
exactement ce qu'une sonde doit tolérer, un service pas encore prêt pouvant ne
pas répondre du tout.
CommandResult porte maintenant failure, qui nomme pourquoi la commande n'a pas
tourné, séparément d'un code de retour non nul. Les deux appellent des gestes
opposés : lire stderr d'un côté, installer un paquet ou réessayer de l'autre.
Corriger la racine a suffi à réparer la sonde et le catalogue, qui utilisaient
déjà check=False et testaient res.ok.
git et docker deviennent des contrôles de doctor. Aucun des deux n'est une
dépendance Python et aucun n'était déclaré nulle part. git est requis partout
puisque catalog add clone ; docker suit ce que le catalogue déclare — requis
dès qu'un lab déclare runtime.services, informatif sinon. Vérifié sur les trois
dépôts : requis sur terraform-training (5 labs) et ansible-training (2),
informatif sur linux-dsoxlab-training (aucun).
Le tirage d'image est séparé du démarrage. Le premier docker run tirait l'image
dans son budget de 180 s : au-delà la commande échouait sur un message de
démarrage qui ne parlait pas du réseau, en deçà run pendait plusieurs minutes
sans dire pourquoi. Le tirage a son propre délai, s'annonce, et est sauté si
l'image est déjà locale.
Un défaut trouvé en vérifiant celui-là, et qui valait d'être corrigé à la
racine : j'avais posé catalog_git_absent dans le code sans la poser dans les
dictionnaires. _() rend alors la clé elle-même, et l'utilisateur voyait
« catalog_git_absent » s'afficher. Mon propre test passait au vert, puisqu'il
vérifiait que le message contenait « git » — que cette clé contient. Le
garde-fou i18n existant s'arrêtait à validators/ et models/ ; il couvre
désormais tout appel _("…") littéral du paquet, dans les deux langues, et il
n'a trouvé que cette clé-là. Le test fautif est durci plutôt que réécrit.
Vérifié : 809 tests dont 19 neufs, éprouvés par trois mutations distinctes,
18 e2e, ruff, mypy strict, et le symptôme d'origine rejoué sur un PATH sans git
dans les deux langues.
Closes #174
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…us prêt (0.1.78) _services_degrades rendait une liste vide quand docker_available() était faux. L'intention était juste — une machine sans Docker n'est pas un lab cassé — mais elle couvrait un cas de trop : « Docker était là et son démon est tombé » devenait indistinguable de « Docker n'a jamais été installé ». Dans le premier cas le lab est bel et bien injouable, et status annonçait pourtant ready, voire validated quand une note existait d'une session précédente. Le moteur est maintenant interrogé en amont, par calculer, et les deux causes se distinguent : docker absent du PATH, ou client présent avec un moteur muet. Elles appellent des gestes opposés — installer un paquet, ou démarrer un démon et vérifier que le compte peut lui parler — et confondre les deux envoie l'utilisateur chercher un paquet déjà présent. Renversement assumé, comme pour #177. Un test affirmait l'inverse : « une machine sans Docker n'est pas un lab cassé : on ne sait simplement rien ». Un lab qui DÉCLARE des services et dont le moteur ne répond pas est injouable, et run y échoue déjà explicitement en services_docker_absent, code 2 : annoncer ready contredisait la commande suivante. Le test est réécrit, porte le motif du renversement, et la moitié juste de la décision d'origine est gardée dans un second test — un lab sans service ignore le moteur, sans même payer une sonde. Les deux détails passent par des appels _() littéraux plutôt qu'une clé calculée : c'est ce qui permet au garde-fou i18n ajouté en 0.1.77 de vérifier qu'ils existent des deux côtés. Il l'a fait. Vérifié sur un lab réel de terraform-training : le même lab passe de validated à degraded avec la cause nommée, en français et en anglais, et sa note reste lisible. 816 tests dont 8 neufs, éprouvés par deux mutations, 18 e2e, ruff, mypy strict. Closes #179 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
doctor sortait en 0 quel que soit l'état de ses contrôles, y compris requis. Le choix était assumé en commentaire et il se défend pour un humain : un diagnostic n'est pas un échec. Mais il rendait la commande inutilisable comme portail automatisé, un script devant analyser le JSON pour savoir si quelque chose manquait. Deux codes plutôt qu'un, parce qu'il y a deux situations et qu'elles appellent des gestes différents. 9 quand un requis a échoué, c'est établi et cela se répare. 10 quand un requis n'a pas pu être mesuré, et cela se remesure. Un environnement dont une sonde n'a pas abouti n'est pas validé pour autant : c'est exactement ce qu'une construction d'image ne doit pas confondre avec un succès, et c'est le fil de tout ce lot d'issues. Quand les deux coexistent, 9 l'emporte, une certitude étant plus forte qu'une ignorance. failing() écarte délibérément les unknown pour ne pas peindre en rouge ce qu'on ignore, et c'est juste pour un tableau que lit un humain. indetermines() est son pendant, pour un appelant qui doit distinguer « c'est bon » de « je n'ai pas pu voir ». Le comportement par défaut ne bouge pas, c'est l'autre moitié du contrat, et deux tests le tiennent. --strict ne change rien d'autre : le tableau et le document restent rendus avant que le code ne tombe, comme validate-structure, pour qu'un appelant recevant un code non nul puisse encore lire ce qui n'allait pas. Un test le vérifie en lisant le JSON d'une invocation sortie en 9. Avec --fix, le rapport est recollecté avant le verdict : juger sur l'état d'avant ferait sortir en échec un --fix qui vient de réussir. Un informatif en échec ne fait pas échouer le portail : c'est l'invariant d'agnosticisme appliqué au code de sortie, un hyperviseur inutile ici n'ayant pas à bloquer un script. docs/machine-output EN + FR portent la table des codes, et la phrase du préambule qui affirmait « doctor sort en 0 » est corrigée dans les deux. fullhelp EN + FR décrivent l'option. Vérifié : 825 tests dont 9 neufs, éprouvés par trois mutations distinctes, 18 e2e, ruff, mypy strict, et les deux modes joués sur terraform-training avec un PATH sans git — défaut 0, --strict 9, --strict --json 9 avec le document lisible. Closes #176 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ision sur les images (0.1.80)
La CI couvrait le lint, mypy, les tests unitaires, le fuzzing et une suite e2e
sur la roue installée, mais ne jouait aucun terraform validate nulle part, aucun
provisionnement, et sa suite e2e ne joue que le lab de démonstration, qui est
shell. Les templates sont épinglés en ~> 0.9 (kvm) et ~> 0.3 (incus) : une
version mineure du provider peut casser le schéma, et le dépôt a déjà vécu ce
cas, les commentaires de kvm/main.tf documentant la rupture 0.8 → 0.9.
Les tests unitaires existants sur ces templates sont des assertions textuelles :
ils vérifient qu'un fichier contient une chaîne, pas que Terraform sait le lire.
Une régression ne se découvrait donc que chez un apprenant, en langage
Terraform.
Le job joue init -backend=false puis validate sur les trois providers, collecte
tous les échecs plutôt que de s'arrêter au premier, nomme le provider fautif par
::error::, et fait échouer la construction. Éprouvé en cassant réellement le
template incus dans une copie du dépôt : deux providers restent valides, incus
est nommé, code 1.
Deux choix qui méritent d'être dits. Terraform vient de l'archive de l'éditeur
avec la somme de contrôle qu'il publie, sur le patron déjà en place pour
poutine : aucune action tierce n'entre dans la chaîne d'approvisionnement pour
un binaire lancé deux fois, et la version est épinglée. Et la validation se fait
en place, par -chdir : le template outscale atteint son cloud-init par
${path.module}/../../cloud-init/, si bien que copier les seuls .tf dans un
répertoire isolé fait échouer un template parfaitement valide. C'est le faux
rouge rencontré en écrivant ce job, et le commentaire du workflow le dit pour
que personne ne le refasse.
Aucun tfvars n'est nécessaire, et ce n'est pas un oubli : validate contrôle la
configuration et ne résout jamais les valeurs de variables, seul plan le ferait
et plan exige un hyperviseur joignable. Vérifié.
La décision sur les images amont est écrite dans le README des templates. Les
sept URL pointent des chemins mutables, et c'est un choix : une somme épinglée
que personne ne tient à jour servirait aux apprenants une image de plus en plus
périmée, avec ses vulnérabilités connues, et le durcissement deviendrait le
vecteur du problème qu'il prétend traiter. Les raisons qui rendent le risque
acceptable — HTTPS sur les domaines officiels, VM jetable, réseau local — et les
conditions qui inverseraient la décision sont explicitées.
Neuf tests gardent le job lui-même, parce qu'un job se supprime aussi
silencieusement qu'un contrôle. Ils ne rejouent pas terraform : un test qui se
sauterait faute de binaire rendrait un vert qui ne prouve rien. La liste des
providers attendus est dérivée du disque, donc un quatrième provider fera rougir
la suite tant qu'il ne sera pas couvert.
Vérifié : 834 tests dont 9 neufs, éprouvés par trois mutations du workflow,
ruff, mypy strict, et les trois analyseurs de chaîne CI joués localement —
actionlint silencieux, zizmor « No findings », poutine « Passed » partout.
Closes #175
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeQL signalait py/incomplete-url-substring-sanitization en haute sévérité sur `assert "releases.hashicorp.com" in script`. Le risque est nul ici — c'est un test qui cherche une chaîne dans un script de workflow, pas une validation d'URL — mais le motif signalé est réellement dangereux ailleurs : « evil.com/releases.hashicorp.com » satisfait la même sous-chaîne. Plutôt que de supprimer l'alerte, l'assertion est ancrée sur l'assignation entière. Elle devient du même coup plus précise : elle vérifie que c'est bien `base` qui vaut cette URL, et non que le domaine est mentionné quelque part. La mutation qui l'éprouve mord toujours : remplacer l'URL par un autre hôte rend le test rouge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…(0.1.81) wait_for_hosts_ready jouait « cloud-init status --wait >/dev/null 2>&1 || true » : l'état et le code de retour partaient tous les deux à la poubelle. Hors ligne, derrière un proxy ou sur un miroir lent, les quinze paquets du premier démarrage ne s'installent pas, cloud-init finit en degraded, et l'hôte était tout de même déclaré prêt. Les labs échouaient ensuite sur des commandes absentes, sans que rien ne relie les deux. Ne pas bloquer reste la bonne décision, et elle est conservée : ce qui compte pour rendre la main, c'est que cloud-init ait terminé. Mais terminer mal doit se dire. L'attente remonte donc le code de retour et la sortie de status --long, que provision affiche à l'écran en nommant l'hôte — ce qui rend le geste de reprise immédiat. Le risque de ce mécanisme est qu'un marqueur change d'un côté et pas de l'autre : le lecteur rendrait alors None sur chaque hôte, donc un silence indistinguable d'un succès, c'est-à-dire le défaut même qu'on corrige. Un test fait parler les deux moitiés ensemble, et la commande est extraite en fonction pour qu'il éprouve ce qu'elle envoie plutôt que la façon dont le fichier est écrit. doctor gagne un contrôle egress, pour dire le problème avant de provisionner plutôt que trois labs plus tard. Les miroirs sondés sont lus dans les templates packagés, jamais écrits dans le moteur, et un test le vérifie sur la source : ce qui est en cause est l'accès sortant lui-même, donc un seul miroir joignable suffit à conclure — exiger les trois ferait rougir un poste sain dont un miroir est en maintenance. Requis sur un dépôt qui provisionne des VM, informatif sinon, vérifié sur les trois cas. La décision sur les paquets est écrite dans le README des cloud-init, avec ce qui a été écarté. Bloquer sur un degraded : cloud-init rend un état global, et traiterait un tree absent comme un lvm2 absent — un blocage sans granularité se contourne. Pré-cuire les images : c'est la vraie réponse, mais un projet à part entière. Déclarer les paquets lab par lab : ce serait le plus juste sur le papier, et cela changerait le contrat v1, gelé, pour un bénéfice que les deux autres mesures obtiennent sans rien casser. Mesuré avant d'être écrit : 15 paquets sur AlmaLinux, 14 sur Debian et Ubuntu. Vérifié : 847 tests dont 13 neufs, éprouvés par trois mutations dont celle du désaccord entre les deux moitiés, 18 e2e, ruff, mypy strict, et le contrôle egress joué dans les deux conditions réseau — failed sous coupure, ok hors coupure, ce qui prouve qu'il mesure au lieu d'échouer toujours. Closes #178 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ons en test Défaut introduit par le commit précédent, trouvé en jouant la suite sous charge : le contrôle egress ouvrait de VRAIES connexions pendant les tests unitaires. Six fichiers appellent collect_checks sans le savoir, chacun payant jusqu'à trois délais d'attente et rendant un verdict différent selon que le réseau répondait ou non. Les mêmes tests passaient sur un runner GitHub et échouaient derrière un pare-feu — c'est-à-dire qu'ils ne mesuraient plus ce qu'ils prétendaient. test_doctor.py portait déjà la règle, et son commentaire disait le défaut mot pour mot : « sans cela, ces tests mesurent la machine qui les exécute : ils passaient en local et échouaient sur un runner CI qui ne les a pas ». La sonde réseau rouvrait la brèche, en pire. La sonde est extraite en _joignable(), et un tests/conftest.py la neutralise pour toute la suite. Neutraliser socket.create_connection aurait été plus court et faux : runtimes/services.py s'en sert pour attendre qu'un conteneur écoute, et le rendre toujours vrai ferait passer des tests de services qui ne prouveraient plus rien. Les deux tests qui éprouvent le contrôle lui-même repatchent _joignable par-dessus. Vérifié dans les deux sens, ce qui est le seul contrôle qui vaille ici : sans le conftest et réseau coupé, deux tests de doctor échouent ; avec, la suite rend 847 verts réseau coupé ET réseau ouvert. Le verdict ne dépend plus du réseau. test_doctor.py passe au passage de plusieurs secondes à 0,21 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…0.1.82) Deux tests de test_services.py échouaient par intermittence, toujours sous charge, jamais au repos. Leur point commun : ils attendaient un vrai conteneur. Sans sonde, start rend la main dès que docker run a répondu, ce qui ne dit rien de l'état du service à l'intérieur — l'étape suivante devient une course, gagnée au repos et perdue sous charge. Ce que la mesure a rendu, et il faut le dire tel quel : l'instabilité ne se reproduit plus. Sous charge Docker soutenue, 12 exécutions ciblées et 4 suites complètes, aucun échec. Le ready_exec ajouté dans une PR antérieure l'a vraisemblablement corrigée, et le commentaire du test l'énonçait déjà. Je n'ai pas pu mesurer la cause d'un défaut que je ne reproduis pas, et je préfère l'écrire plutôt que de proposer une explication que rien n'établit. Ce qui est ajouté est donc un garde-fou contre la RÉGRESSION de cette correction : un test qui démarre un vrai conteneur sans déclarer ready_exec ni ready_tcp fait rougir la suite. Les exemptions sont nommées une par une et motivées — un conteneur qui meurt par conception ne peut pas porter de sonde, et l'exiger transformerait le garde-fou en obstacle. Un second test vérifie qu'aucune exemption ne désigne un test disparu : elle survivrait au renommage et couvrirait silencieusement le suivant portant le même nom. Le garde-fou a trouvé un défaut au passage. test_post_start_en_echec_reel_leve_service_error passait pour la mauvaise raison : sans sonde, le ServiceError qu'il attend pouvait venir du conteneur pas encore prêt à recevoir un docker exec, plutôt que de la commande en échec qu'il prétend prouver. Il déclare maintenant une sonde, et vérifie que l'erreur nomme la commande fautive. Vérifié : 850 tests dont 3 neufs, éprouvés par trois mutations (sonde retirée, exemption orpheline, détection cassée), 18 e2e, ruff, mypy strict. Closes #155 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ais (0.1.83) Le journal mélangeait le français et l'anglais. Ce n'est pas une question de goût : c'est le fichier que dsoxlab support collecte et qu'un utilisateur colle dans un rapport de bug. Mesuré avant d'être corrigé, par lecture AST des 45 appels logger.* : 41 messages français contre 4 anglais. Les appels logger.* restent délibérément hors du garde-fou i18n, et cette exclusion tient toujours : un message de journal n'est pas un texte d'interface, il ne passe par _() nulle part, et le traduire à l'exécution rendrait deux rapports de bug incomparables selon la locale de qui les produit. Ce que l'exclusion justifiait, c'était de ne pas le traduire — pas de le laisser incohérent. Le commentaire du garde-fou annonçait d'ailleurs ce lot mot pour mot ; il est mis à jour. L'anglais l'emporte pour trois raisons, dans l'ordre de poids. Un message de journal se cherche mot pour mot dans un moteur de recherche. Il se compare entre deux machines aux locales différentes. Et il est lu par quelqu'un qui diagnostique, pas par quelqu'un qui apprend — à côté des sorties de terraform, ansible et virsh, qui sont déjà anglaises. Chaque traduction est écrite à la main plutôt que devinée, et un contrôle compare les marqueurs de format avant et après : un %s perdu casse le rendu au moment précis où quelqu'un diagnostique. La règle est écrite là où un contributeur la rencontre — le gabarit de PR, lu à chaque contribution — et tenue par test_journal_en_anglais.py. Sans test, elle se redéferait ligne par ligne, ce qui est exactement ce qui est arrivé à l'interface avant que son propre garde-fou n'existe. Le détecteur est éprouvé dans les deux sens : deux tests lui montrent du français qu'il DOIT attraper, et cinq messages anglais ou techniques qu'il ne doit pas signaler — un faux positif ferait désactiver le contrôle plutôt que corriger le message. Vérifié sur un journal réel : les lignes produites par list-labs, doctor et status, puis celles de deux chemins d'avertissement (contexte illisible, lab.yaml qui lève), ne portent plus un mot français. 854 tests dont 4 neufs, éprouvés par deux mutations, 18 e2e, ruff, mypy strict. Closes #140 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-langue # Conflicts: # CHANGELOG.fr.md # CHANGELOG.md # pyproject.toml # src/dsoxlab/infra/inventory.py # src/dsoxlab/infra/terraform.py # src/dsoxlab/runtimes/shell.py # uv.lock
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Le journal mélangeait le français et l'anglais. Ce n'est pas une question de
goût : c'est le fichier que
dsoxlab supportcollecte et qu'un utilisateur colledans un rapport de bug.
Mesuré avant d'être corrigé, par lecture AST des 45 appels
logger.*:41 messages français contre 4 anglais.
Ce qui ne change pas
Les appels
logger.*restent délibérément hors du garde-fou i18n, et cetteexclusion tient toujours : un message de journal n'est pas un texte d'interface,
il ne passe par
_()nulle part, et le traduire à l'exécution rendrait deuxrapports de bug incomparables selon la locale de qui les produit.
Ce que l'exclusion justifiait, c'était de ne pas le traduire — pas de le
laisser incohérent. Le commentaire du garde-fou annonçait d'ailleurs ce lot
mot pour mot : « Le journal doit être cohérent — il mélange aujourd'hui le
français et l'anglais, ce qui est un vrai défaut — mais c'est un autre lot. »
Il est mis à jour.
Pourquoi l'anglais
Dans l'ordre de poids, et ce sont les raisons de l'issue :
à côté des sorties de terraform, ansible et virsh, déjà anglaises.
Chaque traduction est écrite à la main plutôt que devinée, et un contrôle
compare les marqueurs de format avant et après : un
%sperdu casse le rendu aumoment précis où quelqu'un diagnostique.
La règle, écrite et tenue
Elle vit là où un contributeur la rencontre — le gabarit de PR, lu à chaque
contribution — et non seulement dans un docstring.
test_journal_en_anglais.pyla tient : sans test, elle se redéferait ligne par ligne, ce qui est exactement
ce qui est arrivé à l'interface avant que son propre garde-fou n'existe.
Le détecteur est éprouvé dans les deux sens : deux tests lui montrent du
français qu'il doit attraper, et cinq messages anglais ou techniques
(
virsh snapshot-create-as,run: %s (cwd=%s),terraform output -json) qu'ilne doit pas signaler — un faux positif ferait désactiver le contrôle plutôt
que corriger le message.
Vérification sur un journal réel
list-labs,doctor,statuslab.yamlqui lève (piège n°4)Type of change
Checklist
Always
uv run ruff check src/dsoxlab tests tests_e2e fuzz scriptspassesuv run mypy src/dsoxlabpasses (strict)uv run pytestpasses — 854 passed, dont 4 neufs, éprouvés par deux mutations (un message repassé au français → 1 rouge ; détecteur aveugle → 1 rouge)uv run pytest tests_e2epasses — 18 passedterraform-traininget sur un dépôt d'essai fabriqué pour déclencher les chemins d'avertissementWhen behavior changes
CHANGELOG.mdandCHANGELOG.fr.mdupdateduv.lockrefreshed. Le comportement visible change : ce quesupportramasse n'est plus le même texte.When a command or option is added, removed or changed
N/A — aucune commande ni option n'est touchée. Aucune chaîne d'interface ne
change :
i18n/strings/*.pyest intact, et le garde-fou i18n reste vert.When
.github/workflows/is touchedN/A — seul
.github/PULL_REQUEST_TEMPLATE.mdest modifié, qui n'est pas unworkflow et ne passe par aucun analyseur de chaîne CI.
When the declarative contract (
meta.yml/lab.yaml) changesN/A — le contrat est inchangé.
Related issues
Closes #140
🤖 Generated with Claude Code