Réponse mesurée du producteur, et elle renverse le §13 comme le §19. +etm63 GELAIT la courbe : c'était le seul endroit où le lot était plus faux qu'avant. Les faits établis : issuedAt vient de meteofrance_arome_france_hd, seul domaine AROME exposant un meta.json — qui sert 0 valeur de rayonnement sur 72, donc ne contribue RIEN à la courbe. Le domaine qui la fournit (arome_france) n'expose aucun meta.json (HTTP 500) : il n'existe aucune identité de run pour la donnée qui forme p. Et à 07:30, seamless diverge d'arome_france de 08:00 à 13:00 (144 contre 356, 326 contre 530) et coïncide à partir de 14:00 — le serveur recompose les premières heures autrement que par le seul run. Ce n'est pas un défaut du producteur : c'est ce que l'API permet. LA RÈGLE N'A PAS BOUGÉ, LA CLÉ SI. « Une révision n'existe que si la PRÉVISION a changé » reste intacte ; ce qui était faux, c'est la prémisse — que le run identifie la prévision. Regarder le contenu y est plus fidèle que de regarder une étiquette d'émission. PRIMARY KEY (targetAt, siteId, contentId) firstSeenAt / lastSeenAt : observation, en MILLISECONDES issuedAt / model : attributs, issuedAt pouvant être NUL Migration v1 -> v2 : table REFAITE, pas convertie. Les lignes v1 sont clés sur un run qui n'identifie pas leur contenu et ont pu être écrasées ; leur donner une empreinte calculée sur des valeurs dont on ignore l'origine serait inventer une provenance. TROIS CONSÉQUENCES IMPOSÉES PAR LA MESURE : - issuedAt == 0 n'est PLUS un refus. Il l'a été deux heures, tant qu'il servait de clé. Devenu attribut, refuser une courbe exploitable parce qu'une métadonnée manque reviendrait à jeter la donnée pour son étiquette — et best_match le met à zéro pour toutes ses courbes. Stocké NUL, aucun instant substitué. - siteId entre dans la clé : deux plans publient des courbes différentes sous le même issuedAt, donc la garde d'hier devient NÉCESSAIRE. Sans elle, changer de site désigné mêlerait deux historiques. - t0 ancré sur le jour local de la requête est couvert sans rien ajouter : les targetAt changent, donc l'empreinte aussi. ET UN DÉFAUT TROUVÉ PAR LE TEST, qui n'était pas dans le test : deux révisions écrites dans la même seconde n'étaient pas ordonnables — MAX(firstSeenAt) en rendait deux et la courbe retournait des doublons. Les instants d'observation passent en millisecondes. Un instant d'observation doit distinguer deux observations, sinon il n'ordonne rien ; un cycle par minute l'aurait caché indéfiniment sur la box. Le producteur n'a PAS établi pourquoi le serveur recompose — nowcast, mélange court terme, ou deux domaines sur des runs différents. Il a mesuré QUE ça diverge et SUR QUELLES HEURES. L'explication reste une hypothèse, et rien ici n'en dépend : la clé de contenu est correcte quelle qu'en soit la cause. loadmodel 30/30, charging 48/48, spotmarket 32/32, simulation 69/69, 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
101 lines
4.7 KiB
C++
101 lines
4.7 KiB
C++
// 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 charge utile se REFUSE plutôt que de s'interpréter au mieux — mais pas pour un attribut.
|
|
void testAPayloadIsRefusedOnFormNeverOnAMissingAttribute();
|
|
|
|
};
|
|
|
|
#endif // TESTLOADMODEL_H
|