Patrick Schurig cdd3bc88f3 test(mock): le relais sec, et le cas que le banc ne savait pas produire
La classe `dryRelay` commute et ne mesure pas. Elle ouvre le cas que
l'inventaire à froid avait identifié comme structurellement introuvable : une
charge SAINE et NON MESURÉE. Jusqu'ici `measurement.source: none` n'était
atteignable qu'avec un Thing manquant — ce qui traîne `available: false` et
mélange deux affirmations distinctes, « rien ne mesure cette charge » et
« cette charge est en panne ». Un relais sec est banal sur le terrain, et
parfaitement sain.

Le test vérifie ce que rien ne pouvait vérifier avant : deux charges, même
source: none, available opposés, faultCode sur la seule en défaut. Il
n'aurait pas pu s'écrire hier.

Et l'ajout a révélé que le mock avait SA PROPRE prémisse fausse :
executeAction() déréférençait le contrôleur HTTP de chaque Thing sans
vérifier, supposant que toute classe en possède un. Un relais sec n'en a pas
besoin. Le plantage fut immédiat et net ; l'hypothèse, elle, était invisible
tant qu'aucune classe ne la contredisait — l'homogénéité du mock la
protégeait, exactement comme elle masquait les cas qu'il ne savait pas
produire. Ajouter de la diversité au banc n'a pas seulement ouvert un test :
ça a trouvé un défaut.

AGENTS.md reçoit la règle générale sur les identités : une identité dont les
deux termes viennent de la même source ne vérifie rien. Elle est vraie par
construction et rassure là où rien ne protège — la question n'est pas
« tient-elle ? » mais « de quoi parle-t-elle vraiment ? ». Et c'est le même
défaut que la contre-épreuve qui ne mord pas, vu de l'autre côté : perturber
une grandeur qui ne pouvait rien changer, et vérifier une identité qui se
referme sur elle-même, sont deux gestes de vérification qui ne touchent rien.
Le réflexe commun : demander ce que ce contrôle aurait attrapé s'il avait
échoué. S'il n'y a pas de réponse, il ne contrôle rien.

Suite : simulation 62/62, 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-02 08:00:51 +02:00

88 lines
4.9 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.** `currentPhaseX` et `currentPowerPhaseX` sont posés
ensemble par le mock, dans un rapport fixe de 230. Aucun scénario ne peut produire une tension
réelle ≠ 230 V, ni un compteur qui ne publierait qu'une des deux familles.
**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 borne triphasée à phases déséquilibrées.** Les états existent, mais aucun helper de test
ne les pose indépendamment — d'où le pessimisme de `LM-1210-a` qui n'a jamais été exercé sur un
vrai déséquilibre.
## 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.