Patrick Schurig 04852c518a test(mock): le compteur intègre enfin, et purchasedWh gagne un second usage
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.

Quatrième occurrence d'un mock plus homogène que la réalité, après les six
états cohérents du rootmeter, les classes portant toutes une puissance, et la
triphasée équilibrée.

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, celui-là même que la garde de sémantique traite comme une anomalie. Le
mock fabriquerait le défaut que le moteur est censé détecter.

LM-1014-b appliquée au mock, question posée et répondue : le cumul est
déclaré `cached`, donc il continue au redémarrage. Repartir de zéro ferait
reculer un cumul monotone.

Et purchasedWh gagne un second usage, sous `measured` cette fois. Mesuré sur
le banc : il accumule pendant que deliveredWh peut rester plat, et cette
divergence est un contrôle que l'écran n'aurait pas su fabriquer — de
l'acheté qui monte avec du livré plat signifie soit que la charge ne consomme
pas ce qu'on paie, soit que le compteur ne compte pas. C'est écrit au contrat
parce que le jour où les deux coïncideront, quelqu'un jugera le champ
redondant : c'est précisément quand ils divergent que la paire devient
informative, et un champ qui ne sert que dans l'anomalie n'a pas l'air utile
le reste du temps.

La borne basse du test de cumul est large, et délibérément : il ne contrôle
ni la cadence du minuteur ni l'alignement du premier tic, qui n'intègre rien.
Une borne serrée en ferait un test instable — qu'on relancerait jusqu'à ce
qu'il passe.

Suite : simulation 66/66, 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-04 06:19:08 +02:00

119 lines
7.0 KiB
Markdown

# 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é.
**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.
## 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.