Patrick Schurig 6b1dd95e2e SetLoadConfig : libellé de charge borné à 256 caractères, à l'écriture seulement (Q7)
Le libellé part tel quel dans chaque GetPlan. Rien ne le bornait : une saisie de plus de
1 Mio suffisait à faire dépasser la limite de ligne du protocole. La borne arrête la
saisie en amont ; le refus d'émission du client (commit précédent) reste le filet.

- LoadConfigStore::validateLabels(), appelée en tête de setConfigs() : un libellé de plus
  de 256 points de code Unicode → refus en bloc, rien n'est modifié ; SetLoadConfig rend
  EnergyErrorInvalidParameter et journalise le motif en français, qui nomme la charge par
  son id et donne la longueur, jamais le libellé. Placée AVANT isValid() parce que les
  motifs suivants citent le libellé.
- Même partage que les rangs (LM-1209-a-bis) : load() ne l'applique pas. Un libellé plus
  long déjà persisté est chargé tel quel, ni tronqué ni écarté, et signalé ; la première
  écriture devra le raccourcir.
- Test : testLabelIsBoundedOnWriteNeverOnLoad (256 accepté, 257 refusé sans modification
  en mémoire ni sur disque, configuration héritée conservée entière).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:07:32 +02:00

175 lines
9.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef TESTLOADMODEL_H
#define TESTLOADMODEL_H
#include <QObject>
/*!
* \brief Logique pure du modèle de charge : table de paliers et identité matérielle.
*
* Aucun serveur nymea ici. Ce qui est éprouvé ne dépend ni d'un ThingManager ni d'un
* cycle d'arbitrage : la dérivation des paliers est une combinatoire, et
* \c EnergyArbitrator::sameHardware() une comparaison.
*/
class TestLoadModel : public QObject
{
Q_OBJECT
private slots:
//! La table dérive de TOUTES les sommes de sous-ensembles, triées, 0 compris.
void testStageTableIsDerivedFromSubsetSums();
//! Puissances distinctes : l'ordre de déclaration n'a aucun effet observable.
void testRelayOrderIsInertWhenPowersAreDistinct();
//! Puissances égales : l'ordre décide QUEL contact sert le palier — et ça se voit.
void testRelayOrderDecidesWhichContactServesATiedStage();
//! Un réordonnancement sans effet ne doit PAS coûter une reconstruction.
void testReorderingDistinctRelaysKeepsTheAdapter();
//! Mais un échange de contacts à puissance égale, si.
void testSwappingTiedRelaysRebuilds();
//! Libellé, rang et besoins se mettent à jour en place : jamais de reconstruction.
void testSoftFieldsNeverRebuild();
// ── §11 — compteur et sonde par charge ─────────────────────────────────
//! Les deux champs traversent l'aller-retour, et sont OMIS quand ils sont vides.
void testMeasurementThingsRoundTrip();
//! Ils ne sont pas des canaux de commande : ils n'entrent pas dans claimedThingIds().
void testMeasurementThingsAreNotClaimed();
//! Donc deux charges peuvent partager le même compteur et la même sonde.
void testTwoLoadsMayShareAMeter();
//! Et les changer ne reconstruit rien : ils n'ont aucun effet sur le matériel.
void testMeasurementThingsAreSoftFields();
// ── §12 — le plancher de modulation ────────────────────────────────────
//! Déclaré en dynamic, et il traverse l'aller-retour.
void testFloorIsDeclaredForContinuousLoads();
//! Interdit là où il se DÉRIVE : deux sources d'une même vérité divergent.
void testFloorIsRefusedWhereItWouldBeDerived();
//! Un plancher au-dessus du plafond rendrait la charge inatteignable.
void testFloorAboveCeilingIsRefused();
// ── 3g-2 — le rang de la borne vit dans LoadConfig (LM-1206/LM-1207) ───
//! Une entrée evcharger est valide SANS charge utile, et traverse l'aller-retour.
void testEvChargerConfigIsRankOnly();
//! Toute charge utile d'un autre mécanisme y est refusée (LM-300, union discriminée).
void testEvChargerConfigRefusesPayloads();
//! Elle revendique son Thing : une borne ne peut pas être aussi une charge pilotée.
void testEvChargerClaimsItsThing();
// ── §12 / LM-1209 — l'auto-provisionnement remplit un trou, il ne décide pas ───
//! L'unicité de rang REFUSE une écriture, mais n'écarte JAMAIS au chargement : les deux
//! vérifications sont séparées parce que leurs gestes le sont (LM-1209-a).
void testRankUniquenessRefusesWriteButNeverDiscards();
//! La marque de rang s'efface sur un RANG changé, pas sur une écriture reçue — et un
//! client ne peut pas fabriquer un "auto" (LM-1209-c).
void testRankOriginFollowsTheRankNotTheWrite();
// Protocole optimiseur 1.2, Q7 — le libellé est borné à l'écriture, jamais au chargement
void testLabelIsBoundedOnWriteNeverOnLoad();
// ── LM-1006-1 — le plafond de soutirage, étape 1 ───────────────────────
//! On compare des MARGES, jamais des plafonds : le plus contraignant mord, et on sait
//! lequel. Aucun plafond déclaré ≠ marge nulle. Départage stable à marge égale.
void testEcoSpreadHardensAndSaysSo();
void testEcoUnmeasurableBuysAtAConstantRate();
void testMinimumIsServedEvenWhenTheObligationIsMet();
void testStoredInstantsSurviveATimeZoneChange();
void testReportingDayBoundaryIsPublishedNotDeducible();
void testDrawMarginComparesHeadroomsNotCaps();
//! Une révision n'existe que si la PRÉVISION a changé — et le contenu seul en décide.
void testTheContentIsTheRevisionIdentity();
//! La courbe se relit telle qu'elle était connue, et ses trous restent des trous.
void testTheCurveIsReproducibleAndKeepsItsHoles();
//! La rétention garde les DEUX bouts : n'en garder qu'un détruirait la révision.
void testRetentionKeepsBothEndsOfEachRevision();
//! La courbe COURANTE est la dernière VUE, pas la dernière apparue.
void testTheCurrentCurveIsTheLastSeenNotTheLastNew();
//! La charge utile se REFUSE plutôt que de s'interpréter au mieux — mais pas pour un attribut.
void testAPayloadIsRefusedOnFormNeverOnAMissingAttribute();
void testTransportV2IsReadAndAnUnknownCorrectionRefused();
void testTheSiteCurveIsClippedAfterTheSum();
//! Brief 3 T2 — une ligne par pas ÉCOULÉ et par plan ; v1 laisse gti/tempC/paramètres v2 NULL.
void testTheLearningLogWritesOneRowPerElapsedStepAndPlan();
//! Mesure, écrêtage, neige : LUS ou NULL, jamais devinés.
void testTheLearningLogReadsMeasureCurtailmentAndSnow();
//! « J−1 18:00 locale » et H−1 : la révision du site en vigueur, par sa clé de contenu.
void testTheLearningLogPointsAtTheRevisionsInForce();
//! Base abîmée : table refaite, journalisé, le service continue ; rétention 400 jours.
void testTheLearningLogSurvivesADamagedTable();
//! L'écriture ne bloque jamais l'appelant, même base verrouillée.
void testTheLearningLogNeverBlocksTheCycle();
//! etm68 — une table etm67 RÉELLE est migrée en place : lignes conservées, colonnes NULL.
void testTheLearningLogMigratesAnEtm67TableInPlace();
//! etm68 — le GTI observé va au pas qu'il moyenne, complète seulement ses colonnes, si NULL.
void testTheLearningLogAttachesTheObservedGtiToItsStep();
// ── Brief 3 T4 — tarif saisi ───────────────────────────────────────────
//! Validation STRICTE : prix ≥ 0, plages lisibles, 24 h sans trou ni chevauchement — refus codés.
void testTheTariffIsValidatedStrictly();
//! Les plages sont des heures MURALES : jours de 23 h et de 25 h, oracle hors Qt.
void testTheTariffFollowsLocalTimeAcrossDstDays();
//! Un créneau à cheval sur deux zones reçoit la moyenne pondérée par la durée.
void testAStraddlingSlotGetsTheTimeWeightedMean();
// ── Brief 3 T4 — consommation de base ──────────────────────────────────
//! La minute retranche chaque charge selon sa source ; commande inconnue → minute écartée.
void testTheBaseloadMinuteSubtractsEachLoadBySource();
//! Profil = médiane par créneau local et type de jour sur 28 jours ; qualité dite ; 23 h / 25 h.
void testTheBaseloadProfileIsTheMedianPerSlotAndDayType();
//! Sous 7 jours couverts (à 90 %) : `none`, et rien d'inventé.
void testTheBaseloadIsNoneUnderSevenDays();
//! Tables dédiées, écriture asynchrone, recalcul, rétention — sans toucher forecastLearning.
void testTheBaseloadLogWritesWithoutTouchingTheOtherTables();
// ── Brief 4 T1 — sérialiseur GetPlan (OPTIMIZER_PROTOCOL 1.1 §5) ───────
// Corps dans testoptimizerprotocol.cpp ; références dans data/optimizer/.
//! Golden : site sans PV — grille, base sans profil et tarif saisi, aucune courbe.
void testGetPlanParamsGoldenSiteSansPv();
//! Golden : PV + ECS relay-router + borne + SG-Ready, batterie, dérogation à cible.
void testGetPlanParamsGoldenPvEcsBorneSgReady();
//! Golden : prévision absente (motif) → `pv: null` ; sans instantané → `forecast: null`.
void testGetPlanParamsGoldenPrevisionAbsente();
//! Golden : charge `available=false` figée, et borne configurée sans appareil.
void testGetPlanParamsGoldenChargeIndisponibleFigee();
//! Inconnu = null ou absent, jamais 0 ; ni `group`, ni champs réservés, ni `force`.
void testGetPlanParamsNeverInventsAValue();
//! `meter.exportW` est la mesure brute (règle 8).
void testGetPlanParamsMeterIsRaw();
//! Wh par pas → puissance moyenne ; une case sans point reste null.
void testForecastWhPerStepBecomesMeanPower();
// ── Brief 4 T5 — catalogue des motifs (corps dans testdecisioncatalogue.cpp) ──
//! Anti-dérive : knownCodes() == les constantes de l'en-tête, chacune avec un rendu connu.
void testDecisionCodesListIsComplete();
//! Liste blanche de l'optimiseur (E14) : ni sécurité, ni verrou, ni état du moteur.
void testOptimizerMayEmitOnlyWhitelistedCodes();
//! ECO_FLOOR_BELOW_STAGE se rend « rien n'est acheté », avec ou sans obligation en énergie.
void testEcoFloorBelowStageIsRenderedAsNoPurchase();
// Brief 4 T3 — plan d'un optimiseur (OptimizerPlan, fonctions pures)
void testOptimizerParamKeysMatchInterface();
void testOptimizerPlanIsConverted();
void testOptimizerPlanVerdicts_data();
void testOptimizerPlanVerdicts();
void testOptimizerReasonIsFiltered();
void testOptimizerForceAndReservedAreIgnored();
void testOptimizerUnavailableLoadKeepsTheShadow();
void testOptimizerFinaliseUsesTheAppliedAction();
};
#endif // TESTLOADMODEL_H