Skip to content

refactor(logging): le journal parle une seule langue, et c'est l'anglais (0.1.83) - #192

Merged
stephrobert merged 12 commits into
mainfrom
fix/journal-une-seule-langue
Aug 24, 2026
Merged

refactor(logging): le journal parle une seule langue, et c'est l'anglais (0.1.83)#192
stephrobert merged 12 commits into
mainfrom
fix/journal-une-seule-langue

Conversation

@stephrobert

Copy link
Copy Markdown
Owner

Summary

Empilée sur #184#191. Une fois celles-ci mergées, cette PR se réduit à
son seul commit.

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.

Ce qui ne change pas

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 : « 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 :

  1. un message de journal se cherche mot pour mot dans un moteur de recherche ;
  2. il se compare entre deux machines aux locales différentes ;
  3. il est lu par quelqu'un qui diagnostique, pas par quelqu'un qui apprend —
    à 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 %s perdu casse le rendu au
moment 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.py
la 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'il
ne 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

Ce qui a été joué Mots français dans le journal
list-labs, doctor, status 0
contexte illisible (chemin d'avertissement) 0
lab.yaml qui lève (piège n°4) 0

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Chore / tooling
  • Security / supply chain

Checklist

Always

  • uv run ruff check src/dsoxlab tests tests_e2e fuzz scripts passes
  • uv run mypy src/dsoxlab passes (strict)
  • uv run pytest passes — 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_e2e passes — 18 passed
  • The engine stays domain-agnostic — seuls des messages de journal changent
  • No hardcoded personal path or host
  • Manually tested — journal produit sur terraform-training et sur un dépôt d'essai fabriqué pour déclencher les chemins d'avertissement

When behavior changes

  • Both CHANGELOG.md and CHANGELOG.fr.md updated
  • Version bumped (0.1.82 → 0.1.83) and uv.lock refreshed. Le comportement visible change : ce que support ramasse 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/*.py est intact, et le garde-fou i18n reste vert.

When .github/workflows/ is touched

N/A — seul .github/PULL_REQUEST_TEMPLATE.md est modifié, qui n'est pas un
workflow et ne passe par aucun analyseur de chaîne CI.

When the declarative contract (meta.yml / lab.yaml) changes

N/A — le contrat est inchangé.

Related issues

Closes #140

🤖 Generated with Claude Code

stephrobert and others added 12 commits August 24, 2026 17:36
…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
@stephrobert
stephrobert merged commit dd6b880 into main Aug 24, 2026
19 checks passed
@stephrobert
stephrobert deleted the fix/journal-une-seule-langue branch August 24, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P3] Le journal mélange français et anglais, et c'est lui que support collecte

1 participant