Patrick Schurig f3cddedee0 test(mock): la tension par phase, et LM-1210-a enfin exercée
Le mock posait currentPhaseX = currentPowerPhaseX / 230 : les deux familles
d'états étaient cohérentes par construction, donc ni triphasée déséquilibrée
ni tension ≠ 230 V n'étaient fabricables. Les paramètres sont optionnels —
sans eux le comportement d'origine tient, et aucun scénario existant ne bouge.

LM-1210-a est enfin exercée : le choix de la pire phase faute de masque était
écrit, raisonné, et jamais vérifié. Phase B saturée à 30 A sous un plafond de
25 pendant que A et C exportent, et la maison exporte GLOBALEMENT — la marge
retenue est celle de B, négative, et non celle du total qui rassurerait.

Et l'ouverture a corrigé une ERREUR DE RAISONNEMENT, pas seulement comblé un
trou. Mon premier test attendait qu'une phase en export donne une marge
supérieure au plafond : c'est ce que le design affirmait. Il a échoué —
calculateAllowanceAmpere() part du plafond et ne fait que des qMin, donc la
marge y est écrêtée. Physiquement juste, mais la phrase était fausse. La
conclusion tenait, la raison non, et rien ne pouvait le révéler tant que le
déséquilibre était infabricable. Corrigée dans la spec et dans le code.

C'est la deuxième fois qu'ajouter de la diversité au mock trouve un défaut
plutôt que de seulement débloquer un test — après dryRelay, qui avait révélé
le déréférencement du contrôleur.

Et une note à côté de la garde du compteur : l'app dépend de `connected` pour
REFUSER D'AFFICHER, jamais pour COMPTER, et elle a raison. Si connected ment,
l'écran dit « hors ligne » à tort et rien n'est mal compté. Une comptabilité
n'a pas droit à cette tolérance — même donnée non fiable, deux usages, deux
verdicts. S'abstenir sur un doute est sûr, accumuler sur un doute ne l'est
pas. Quelqu'un lirait l'asymétrie comme une incohérence sans cette ligne.

Suite : simulation 64/64, loadmodel 22/22, charging 48/48 identique à la
référence, spotmarket 32/32, doxygen 0 avertissement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-09-03 07:50:56 +02:00

5.9 KiB

INVENTAIRE À FROID DU MOCK — ce qu'il rend structurellement introuvable

(Fait le 2026-09-01, après trois occurrences en trois jours d'un cas réel impossible à reproduire. Ce n'est plus un incident, c'est une propriété du banc d'essai.)

Pourquoi cet inventaire

Trois fois cette semaine, un défaut réel s'est révélé inatteignable par la suite :

Occurrence Ce qui était introuvable Comment on l'a su
Correctif amont calculateAllowanceAmpere un compteur publiant les courants sans les puissances par phase le mock pose currentPhaseB = currentPowerPhaseB / 230, cohérent par construction
Test command sous source: none une charge dont rien ne mesure la puissance toutes les classes pilotables portent currentPower
Test ECO_FLOOR_* en régime unmeasurable une borne sans sessionEnergy il a fallu ajouter l'état à une seule classe pour avoir les deux cas

Le point commun : le mock est plus homogène que la réalité. Il a été construit pour faire marcher des scénarios, pas pour représenter la diversité du parc — et une suite verte ne dit alors rien sur les cas qu'il ne sait pas produire.

Les classes, et ce qu'elles portent

Classe currentPower totalEnergyConsumed sessionEnergy
meter ✅ ✅ —
charger ✅ ✅ ❌
chargerPhaseSwitching ✅ ✅ ✅ (ajouté le 2026-08-31)
simpleCharger ❌ ❌ ❌
powerSwitch ✅ ❌ —
etmVariableLoad ✅ (currentPowerW) ❌ —
energyStorage ✅ ❌ —
car, notification ❌ ❌ —

Ce que la table donne, et c'est la bonne nouvelle : sessionEnergy et totalEnergyConsumed sont désormais discriminants — il existe des classes avec et sans. Les régimes measured / unmeasurable et les causes noSessionEnergy / meterWithoutEnergy sont donc exerçables.

Ce qui reste STRUCTURELLEMENT introuvable

1. measurement.source: none pour une charge saine. — OUVERT le 2026-09-02. La classe dryRelay a été ajoutée : elle commute, elle ne mesure pas. Une charge saine et non mesurée est désormais fabricable, et testHealthyButUnmeasuredDiffersFromFaulty vérifie ce que rien ne pouvait vérifier avant — que le moteur distingue « rien ne mesure cette charge » de « cette charge est en panne », deux affirmations que le seul chemin disponible (un Thing manquant) mélangeait.

Ce que l'ajout a révélé au passage : executeAction() déréférençait le contrôleur HTTP de chaque Thing sans vérifier — le mock supposait que toute classe en possède un. Un relais sec n'en a pas besoin, n'ayant rien à mesurer. Le plantage était immédiat et net ; l'hypothèse, elle, était invisible tant qu'aucune classe ne la contredisait. Le mock avait sa propre prémisse fausse, protégée par son homogénéité.

2. Un compteur par phase incohérent. — OUVERT le 2026-09-03. voltagePhaseX et currentPhaseX sont désormais réglables indépendamment des puissances, par des paramètres OPTIONNELS — sans eux, le comportement dérivé d'origine est conservé et aucun scénario existant ne bouge.

Et l'ouverture a corrigé une erreur de RAISONNEMENT, pas seulement comblé un trou. Le premier test de LM-1210-a attendait qu'une phase en export donne une marge supérieure au plafond — c'est ce que le design affirmait. Il a échoué : calculateAllowanceAmpere() part du plafond et ne fait que des qMin, donc la marge y est écrêtée. Physiquement juste — l'export ne permet pas de tirer plus que le disjoncteur — mais la phrase du design était fausse. La conclusion tenait, la raison non, et rien ne pouvait le révéler tant que le déséquilibre était infabricable.

3. temperature. Le code teste hasState("temperature") et aucune classe du mock ne le porte : la branche « sonde présente » n'a jamais été exécutée. Elle est écrite, elle n'est pas testée.

4. Une triphasée déséquilibrée. — OUVERT le 2026-09-03. testWorstPhaseBindsWhenPhasesAreUnbalanced exerce enfin LM-1210-a : phase B saturée à 30 A sous un plafond de 25 pendant que A et C exportent, et la maison exporte globalement. La marge retenue est celle de B — négative — et non celle du total, qui rassurerait à tort.

Ce manque était nommé deux fois par deux chemins indépendants — cet inventaire et le balayage des contrôles — avant d'être comblé. C'est ce qui lui a donné sa priorité.

La règle qui en sort

Avant d'écrire un test qui exerce une ABSENCE, vérifier que le mock peut la produire. Une assertion sur un cas inatteignable ne passe pas : elle ne s'exécute pas, ce qui est pire — elle compte comme verte.

Le symptôme se reconnaît : la contre-épreuve ne mord pas. C'est le corollaire fin d'AGENTS.md appliqué au banc d'essai plutôt qu'au code.

Et la parade tient en une ligne de test : quand on itère pour trouver des cas, compter combien on en a examinés, et échouer si le compte est nul. C'est ce qui a rattrapé le test de command, et ce qui garde celui de la mesurabilité.

Ce qu'il faudrait ajouter, par ordre d'utilité

  1. Une classe pilotable SANS aucune puissance — un relais sec. Débloque source: none sur une charge saine, et sépare enfin « non mesurée » de « en panne ».
  2. Un compteur sans totalEnergyConsumed en tant que classe déclarée, plutôt qu'un powerSwitch détourné comme aujourd'hui.
  3. Une tension par phase réglable indépendamment des puissances — pour exercer le correctif amont ailleurs qu'en inversant la fonction.

Aucune de ces trois n'est urgente. Ce qui l'est, c'est de savoir qu'elles manquent : sans cette liste, chaque lot repaiera le même temps à découvrir qu'un cas est introuvable.