`meterNoEnergy` remplace le `powerSwitch` détourné en instrument de mesure. Le
détour marchait — le moteur ne teste que `hasStateType("totalEnergyConsumed")` —
mais il modélisait un montage que personne ne fait, et faisait tenir DEUX rôles à
la même Thing dans le même scénario : relais d'une charge et compteur d'une autre.
La classe ne déclare PAS `energymeter` : cette interface EXIGE `totalEnergyConsumed`
et `totalEnergyProduced`. Promettre une interface qu'on ne tient pas ferait mentir
le mock exactement là où il doit être littéral. `smartmeter` est la base sans état
obligatoire, et c'est précisément ce qu'est ce matériel.
`thermalProbe` ouvre le volet thermique. La branche « sonde présente » de
attachMeasurements() était écrite depuis toujours et n'avait JAMAIS été parcourue :
aucune classe du mock ne portait l'état `temperature`. C'était la seule entrée du
volet thermique, et rien ne l'exerçait.
CE QUE L'OUVERTURE A RÉVÉLÉ, comme les trois précédentes. `setupThing()` du mock
n'avait AUCUNE sortie par défaut : une classe sans branche ne rencontrait jamais
d'`info->finish()`. Le setup ne échouait pas — il ne se terminait jamais. Le
symptôme est un Thing éternellement « en cours de configuration » et un test qui
expire ailleurs, très loin de la cause. Même mécanisme que le contrôleur
déréférencé pour toute classe : une hypothèse du mock que son homogénéité
protégeait. Une classe oubliée se dit maintenant, en qCWarning + SetupFailed.
UNE GARDE A MORDU sur son auteur le jour même. La première version du test
thermique lisait `loads[]` sans compteur racine — donc une liste vide, et trois
assertions qui ne se seraient jamais exécutées. `QCOMPARE(parId.count(), 3)` l'a
attrapé. C'est la parade écrite dans INVENTAIRE_MOCK.md, appliquée à elle-même.
RELEVÉ, NON CORRIGÉ : une sonde DÉSIGNÉE mais incapable produit la même charge
utile qu'aucune sonde du tout (`temperatureC` absent), alors que le geste de
réparation diffère — désigner une sonde, ou en changer. Silence de la famille
`progressUnmeasurableCause` ; le fermer demande un catalogue de causes thermiques
qui n'existe pas encore.
Suites : simulation 67/67 (+1), loadmodel 23/23, charging 48/48, spotmarket 32/32.
9.2 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. — OUVERT le 2026-09-02. La classe
measurement.source: none pour une charge saine.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-aattendait 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 desqMin, 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. — OUVERT le 2026-09-05. La classe temperature.thermalProbe porte l'état, et
testThermalProbeFeedsTelemetry exerce enfin la branche « sonde présente » de
attachMeasurements(), écrite depuis toujours et jamais parcourue. C'était la seule entrée du
volet thermique, et rien ne l'exerçait.
Ce qu'il reste à dire, et qui n'est pas corrigé ici : une sonde DÉSIGNÉE mais incapable — un Thing sans état
temperature— produit exactement la même charge utile qu'aucune sonde du tout (temperatureCabsent). Le geste de réparation, lui, diffère : désigner une sonde, ou en changer. C'est un silence de la familleprogressUnmeasurableCause, relevé par le test et laissé ouvert : le fermer demande un catalogue de causes thermiques qui n'existe pas encore.
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.mdappliqué 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é.
5. Un compteur d'énergie FIGÉ. — OUVERT le 2026-09-04. totalEnergyConsumed n'était
écrit par personne : il valait sa valeur par défaut, indéfiniment. Un compteur figé est un
compteur qui n'existe pas pour tout ce qui l'intègre — deliveredWh restait à 0, remainingWh
à 4 000, et l'étalement adaptatif n'était jamais exercé. Le comportement se confondait avec
unmeasurable pour une raison entièrement différente et bénigne.
Le mock intègre désormais currentPower dans le cumul, une fois par seconde. En export il ne
recule pas — il ne bouge pas : un compteur de consommation est monotone, et le faire reculer
produirait un cas que le terrain ne produit pas, que la garde de sémantique traiterait comme une
anomalie.
Et LM-1014-b s'y applique, question posée et répondue : totalEnergyConsumed est déclaré
cached, donc il continue au redémarrage du mock. Repartir de zéro ferait reculer un cumul
monotone — le mock fabriquerait l'anomalie que le moteur est censé détecter.
Quatrième occurrence. Après les six états cohérents du rootmeter, les classes portant toutes
une puissance, et la triphasée équilibrée.
6. Le compteur sans énergie et la sonde thermique. — OUVERTS le 2026-09-05, et les deux
derniers de la liste. meterNoEnergy remplace le powerSwitch détourné en instrument de mesure —
un montage que personne ne fait, et qui faisait tenir DEUX rôles à la même Thing dans le même
scénario. thermalProbe ouvre le volet thermique.
Et l'ouverture a révélé un défaut, comme les trois précédentes.
setupThing()du mock n'avait aucune sortie par défaut : une classe sans branche ne rencontrait jamais deinfo->finish(). Le setup ne échouait pas — il ne se terminait jamais. Le symptôme est un Thing éternellement « en cours de configuration » et un test qui expire ailleurs, très loin de la cause.C'est le même mécanisme que le contrôleur déréférencé pour toute classe : une hypothèse du mock que son homogénéité protégeait. Tant que chaque classe avait sa branche, l'absence de sortie par défaut ne pouvait pas se voir. Corrigé : une classe oubliée se dit maintenant en
qCWarninget rendThingErrorSetupFailed.Et une garde a mordu au passage : la première version du test thermique lisait
loads[]sans compteur racine — donc une liste vide, et trois assertions qui ne se seraient jamais exécutées.QCOMPARE(parId.count(), 3)l'a attrapé. C'est la parade écrite plus haut dans ce document, appliquée le jour même à son auteur.
Ce qu'il faudrait ajouter, par ordre d'utilité
- Une classe pilotable SANS aucune puissance — un relais sec. Débloque
source: nonesur une charge saine, et sépare enfin « non mesurée » de « en panne ». Un compteur sans— FAIT le 2026-09-05 :totalEnergyConsumeden tant que classe déclarée.meterNoEnergy. Elle ne déclare PAS l'interfaceenergymeter, qui exigetotalEnergyConsumed— promettre une interface qu'on ne tient pas ferait mentir le mock là où il doit être littéral.- 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.