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:
parent
aef02c0e9d
commit
24a6c5ef64
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user