test(RPC): balayage des getters — et la contre-épreuve a corrigé mon diagnostic

Le trou nommé hier : la surface RPC n'est couverte par aucun test. Une méthode
qui ne répond plus, en silence, pendant que 31 unitaires, la simulation et
ci-quality passent.

Simulation::testEveryGetterReturnsWhatItDeclares appelle les 14 méthodes
NymeaEnergy.Get* et vérifie, dans cet ordre : le STATUT est success, la charge
utile n'est pas vide, et chaque clé requise de `returns` est servie. Les Set*
sont exclus — ils mutent, et les appeler à l'aveugle serait pire que le trou.

MA PREMIÈRE VERSION ÉTAIT UNE CONTRE-ÉPREUVE INERTE. Elle disait « pour chaque
clé déclarée, vérifier qu'elle est servie » : en retirant la déclaration de
GetPvForecast, elle PASSAIT — il n'y avait plus rien à vérifier. Le défaut
désactivait son propre détecteur, soit exactement le mode d'érosion catalogué
dans AGENTS.md, rencontré dans le contrôle écrit POUR fermer un trou.

Le critère est donc passé de la DÉCLARATION à la RÉPONSE : un getter qui ne rend
rien est cassé, quelle que soit sa déclaration. C'est l'affirmation qui tient
debout sans elle.

ET LA CONTRE-ÉPREUVE A CORRIGÉ MON DIAGNOSTIC. J'avais écrit que nymea élimine
la charge utile quand `returns` n'est pas déclaré. C'est FAUX, vérifié : la
méthode répond correctement sans returns. Ce qui casse, c'est la déclaration des
PARAMÈTRES — sans elle l'appel est rejeté à la validation (« Invalid params:
Invalid key: from ») et le handler n'est jamais atteint.

La différence n'est pas académique : un client qui ne lit que `params` voit une
réponse vide et accuse le handler, qui n'a rien fait. J'avais le bon coupable et
le mauvais mécanisme — conclusion vraie sur un raisonnement faux, et rien ne
l'aurait montré sans une contre-épreuve qui rejoue le défaut. Le contrôle regarde
donc le STATUT en premier, pour nommer le vrai coupable.

Ce que le balayage NE couvre pas, écrit plutôt que sous-entendu : les Set* (non
appelés), la FORME des valeurs (une clé présente ne dit rien de son contenu), et
les paramètres OPTIONNELS (o:knownAt n'est jamais exercé).

simulation 70/70, ci-quality 4/4, build 0 erreur 0 warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
This commit is contained in:
Patrick Schurig 2026-09-09 08:43:19 +02:00
parent aef02c0e9d
commit 24a6c5ef64
3 changed files with 145 additions and 2 deletions

View File

@ -1081,8 +1081,18 @@ Le second plan du §22 était une **fiction de test** du producteur — ajouté
`GetPvForecast` répondait **`{}`** — pas même une liste vide. En réécrivant sa `description` par
une substitution de texte, j'avais emporté les `params.insert(...)` et `returns.insert(...)` qui
la suivaient. nymea valide la réponse contre le schéma déclaré : sans `returns`, **la charge
utile est éliminée en silence**.
la suivaient.
> **MÉCANISME CORRIGÉ le 2026-09-09** — ma première explication était fausse, et la
> contre-épreuve l'a démolie. nymea **ne filtre pas** la charge utile : en retirant le
> `returns`, la méthode répond toujours correctement (vérifié). Ce qui casse, c'est la
> déclaration des **paramètres** : sans elle, l'appel est **rejeté à la validation** —
> `Invalid params: Invalid key: from` — et **le handler n'est jamais atteint**.
>
> La différence n'est pas académique : un client qui ne lit que `params` voit une réponse vide et
> **accuse le handler**, qui n'a rien fait. C'est le mode d'erreur « conclusion vraie sur un
> raisonnement faux » — j'avais le bon coupable et le mauvais mécanisme, et rien ne l'aurait
> montré sans une contre-épreuve qui rejoue le défaut.
> **Aucun test ne couvre la surface RPC.** Les 31 tests unitaires passaient, la simulation
> passait, `ci-quality` passait — et la méthode ne rendait plus rien. Ce n'est pas un défaut de
@ -1122,3 +1132,50 @@ ni le producteur ni moi n'avions prévu : **ajouter puis retirer**.
*(Fenêtre à écarter de toute comparaison prévu/réalisé : **08:07 → 08:11:36**, où la table décrit
une installation fictive. Les deux révisions qui l'encadrent sont des changements
d'INSTALLATION, pas de météo — exactement ce que la colonne différée du §22.2 servirait à dire.)*
---
## 24. Le balayage des getters — et sa première version était inerte
`Simulation::testEveryGetterReturnsWhatItDeclares` appelle **toutes** les méthodes
`NymeaEnergy.Get*` du namespace (14 aujourd'hui) et vérifie trois choses, dans cet ordre :
1. **le STATUT est `success`** — sinon la méthode est nommée avec son erreur ;
2. la charge utile n'est **pas vide** ;
3. chaque clé **requise** de la déclaration `returns` est présente.
Les `Set*` sont exclus : ils **mutent**, et les appeler à l'aveugle serait pire que le trou.
### 24.1 La première version était une contre-épreuve inerte
Elle disait : *« pour chaque clé déclarée, vérifier qu'elle est servie »*. En retirant la
déclaration de `GetPvForecast`, elle **passait** — il n'y avait plus rien à vérifier.
> **Le défaut désactivait son propre détecteur.** C'est exactement le mode d'érosion catalogué
> dans `AGENTS.md`, rencontré ici dans le contrôle écrit *pour* fermer un trou.
Le critère a donc été déplacé de la **déclaration** vers la **réponse** : *un getter qui ne rend
rien est cassé, quelle que soit sa déclaration.* C'est l'affirmation qui tient debout sans elle.
### 24.2 Et la contre-épreuve a corrigé le diagnostic
Rejouée avec le vrai défaut — `params` supprimés — elle mord et **nomme la cause** :
```
NymeaEnergy.GetPvForecast → statut « error » (Invalid params: Invalid key: from)
```
C'est ce qui a établi que le mécanisme n'était pas celui que j'avais écrit (§23.1). **Un contrôle
écrit pour attraper un défaut a servi d'abord à comprendre ce défaut** — ce qu'aucune relecture
n'avait fait, parce que le symptôme était compatible avec deux causes.
### 24.3 Ce que ce balayage ne couvre pas, et il faut le dire
- **Les `Set*`** : non appelés. Le trou reste ouvert de leur côté, et le fermer demande un
scénario par méthode, pas un balayage.
- **La FORME des valeurs** : on vérifie qu'une clé est là, pas que son contenu est du bon type
ni qu'il a du sens. `forecastPoints: []` passe — et c'est correct, une table vide est un état
légitime.
- **Les paramètres OPTIONNELS** : `o:knownAt` n'est jamais exercé. Une déclaration optionnelle
supprimée ne serait pas vue.

View File

@ -7010,6 +7010,90 @@ void Simulation::testAModeWriteEndsTheDerogationAndKeepsTheChoice()
#endif
}
void Simulation::testEveryGetterReturnsWhatItDeclares()
{
// LE TROU QUE LE 2026-09-09 A OUVERT.
//
// `GetPvForecast` a répondu `{}` pendant une heure sur le banc : une réécriture de sa
// description avait emporté ses `returns.insert(...)`, et nymea élimine EN SILENCE toute
// charge utile qui ne correspond à aucun schéma déclaré. Trente et un tests unitaires, la
// simulation et ci-quality passaient — aucun ne regarde la SURFACE RPC.
//
// Ce n'est pas un défaut de logique, c'est un défaut de DÉCLARATION. Il ne se voit qu'en
// APPELANT, et il touche n'importe quelle méthode dont on retouche la description. Le
// contrôle porte donc sur la CLASSE entière, pas sur la méthode qui a mordu.
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
const QVariantMap introspection =
injectAndWait("JSONRPC.Introspect").toMap().value("params").toMap();
const QVariantMap methodes = introspection.value("methods").toMap();
QVERIFY2(!methodes.isEmpty(), "l'introspection n'a rendu aucune méthode");
// Les paramètres REQUIS des rares getters qui en ont. Une table explicite plutôt qu'une
// fabrication automatique : une valeur devinée qui échoue ferait accuser le handler là où
// c'est l'appel qui est faux.
const QHash<QString, QVariantMap> parametres {
{ QStringLiteral("NymeaEnergy.GetPvForecast"),
{{"from", "2026-09-09T00:00:00Z"}, {"to", "2026-09-10T23:00:00Z"}} },
};
int verifies = 0;
QStringList muettes;
for (auto it = methodes.constBegin(); it != methodes.constEnd(); ++it) {
const QString nom = it.key();
if (!nom.startsWith(QStringLiteral("NymeaEnergy.Get")))
continue; // les Set* MUTENT : les appeler à l'aveugle serait pire
const QVariantMap attendu = it.value().toMap().value("returns").toMap();
const QVariantMap brut = injectAndWait(nom, parametres.value(nom)).toMap();
const QVariantMap reponse = brut.value("params").toMap();
++verifies;
// LE STATUT D'ABORD. Le défaut du 2026-09-09 n'était PAS un retour supprimé — vérifié :
// nymea ne filtre pas la charge utile. C'était la déclaration des PARAMÈTRES qui avait
// disparu, si bien que l'appel était REJETÉ à la validation. Un client qui ne regarde
// que `params` voit alors une réponse vide et accuse le handler, qui n'a jamais été
// atteint. Regarder le statut nomme le vrai coupable.
if (brut.value(QStringLiteral("status")).toString() != QLatin1String("success")) {
muettes << QStringLiteral("%1 → statut « %2 » (%3)")
.arg(nom, brut.value(QStringLiteral("status")).toString(),
brut.value(QStringLiteral("error")).toString());
continue;
}
// LE CRITÈRE PORTE SUR LA RÉPONSE, PAS SUR LA DÉCLARATION — et c'est tout le sujet.
//
// Première version : « pour chaque clé déclarée, vérifier qu'elle est là ». La
// contre-épreuve l'a démolie — en retirant la déclaration de GetPvForecast, le contrôle
// ne voyait plus rien à vérifier et PASSAIT. Le défaut désactivait son propre détecteur :
// contre-épreuve inerte, exactement le mode d'érosion que l'AGENTS.md catalogue.
//
// UN GETTER QUI NE REND RIEN EST CASSÉ, quelle que soit sa déclaration. C'est cette
// affirmation-là qui tient debout sans elle.
if (reponse.isEmpty()) {
muettes << QStringLiteral("%1 → charge utile VIDE (déclaration absente ou handler muet)")
.arg(nom);
continue;
}
// Et EN PLUS, quand une déclaration existe, ses clés requises doivent être servies : une
// clé oubliée est le même défaut, en plus discret.
for (auto k = attendu.constBegin(); k != attendu.constEnd(); ++k) {
if (k.key().startsWith(QStringLiteral("o:")))
continue; // optionnelle : son absence est une information
if (!reponse.contains(k.key()))
muettes << QStringLiteral("%1 → « %2 » manquante").arg(nom, k.key());
}
}
QVERIFY2(verifies >= 5,
qUtf8Printable(QStringLiteral("seulement %1 getter(s) parcouru(s) — le balayage "
"ne prouve rien s'il ne balaie rien").arg(verifies)));
QVERIFY2(muettes.isEmpty(),
qUtf8Printable(QStringLiteral("des méthodes ne rendent pas ce qu'elles déclarent :\n %1")
.arg(muettes.join(QStringLiteral("\n ")))));
}
void Simulation::testDerogationTakesTheHeadButNotTheSafety()
{
#ifndef ETM_ARBITRATOR

View File

@ -209,6 +209,8 @@ private slots:
void testARefusedStateIsDistinguishableFromAnApplied();
void testL4ShedsAPacAndSaysItForcedTheLock();
void testDerogationTakesTheHeadButNotTheSafety();
//! TOUTE méthode Get* du namespace rend ce que sa déclaration promet.
void testEveryGetterReturnsWhatItDeclares();
void testAModeWriteEndsTheDerogationAndKeepsTheChoice();
// ── §12 / LM-1209 ──────────────────────────────────────────────────────