diff --git a/docs/DESIGN_PREVISION_PV.md b/docs/DESIGN_PREVISION_PV.md index 462c936..ce9ad9b 100644 --- a/docs/DESIGN_PREVISION_PV.md +++ b/docs/DESIGN_PREVISION_PV.md @@ -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. diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index 6e57574..97bbe5f 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -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 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 diff --git a/tests/auto/simulation/simulation.h b/tests/auto/simulation/simulation.h index 7992e14..7aa748a 100644 --- a/tests/auto/simulation/simulation.h +++ b/tests/auto/simulation/simulation.h @@ -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 ──────────────────────────────────────────────────────