Patrick Schurig 50a1f91feb feat(motifs): OPTIMIZER_DECISION, PRICE_SLOT, knownCodes() et liste blanche optimiseur (brief 4 T5)
- OPTIMIZER_DECISION : code générique d'une action d'optimiseur dont le reasonCode est
  absent, inconnu ou hors liste blanche ; sans paramètres ni texte (invariant 12).
- PRICE_SLOT (priceCtkWh, o:setpointW) : décision prise sur le prix du créneau, demandée par
  l'app (exigence 5). Aucun émetteur interne — le rule-based ne lit pas le prix (§11).
  EV_SPOT_MARKET ne convenait pas : il nomme un fournisseur hors du build et ne vaut que
  pour une borne.
- DecisionCode::knownCodes() : la liste énumérable du catalogue (« connu » se lit ici).
- DecisionCode::optimizerEmittableCodes() / optimizerMayEmit() : liste blanche E14 —
  SURPLUS_SETPOINT, SURPLUS_INSUFFICIENT, BELOW_MIN_POWER, EV_DEADLINE, SG_FORCED,
  SG_RECOMMENDED, SG_NORMAL, ECO_FLOOR_GRID, PRICE_SLOT, OPTIMIZER_DECISION. Jamais la
  sécurité, les verrous, l'indisponibilité, le plafond, la réserve, la dérogation ni les
  états du moteur.
- renderFr : deux branches ; INTERFACE.md : les deux lignes (libellés FR/DE/EN proposés
  pour l'app) et l'encadré de la liste blanche.

Rien n'appelle ces fonctions : sans endpoint, aucun effet.

Tests (loadmodel) : anti-dérive — knownCodes() confronté aux constantes de l'EN-TÊTE lu par
le test, chacune avec un rendu autre que « motif inconnu » ; liste blanche ⊂ catalogue et
sans aucun code moteur. Contre-épreuves : constante ajoutée sans entrée, branche SG_NORMAL
retirée → échec.

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

161 lines
8.8 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();
// ── 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();
};
#endif // TESTLOADMODEL_H