Patrick Schurig 61db6cfd66 test(conformance): mode attendu plan | abstain, un optimiseur qui s'abstient est jugé conforme
S'abstenir est une réponse permise (§7). Avec ETM_POWERSYNC_CONFORMANCE_EXPECT=abstain,
testEveryPlanIsAccepted exige une abstention à chaque cycle (rules, fallbackCode abstain,
verdict abstention/abstain, aucun échec, optimizerAlive inchangé) ; testTheSafetyLoopStaysOnTop
vérifie L4 sous le rule-based (shed exigé) puis se termine en QSKIP, rien à écrêter. Mode `plan`
(défaut) inchangé. Le mode retenu est affiché en tête de chaque fonction.

Pas de déduction depuis le handshake : `planning` est informatif (multi-créneaux ou non) et
`kinds` absent veut dire « tout admis » ; la référence en mode abstain annonce des kinds, le mode
protocol-1.0 (planning false, sans capabilities) planifie. README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:46:53 +02:00
..

etm-powersync-optimizer-conformance — conformité d'un optimiseur quelconque

Suite de test du moteur (Qt Test, harnais nymea du dépôt). Elle lance votre optimiseur, branche dessus le vrai moteur — serveur nymea, arbitre, SocketScheduler, OptimizerClient, adaptateurs, sur les mocks de la suite simulation — et vérifie, une fonction par point :

Fonction Ce qui est vérifié
testHandshakeIsAccepted le client du moteur, directement sur l'optimiseur, passe le handshake (§3) : majeure 1, aucun code d'erreur
testGetPlanIsAnsweredInTime 8 cycles : chaque GetPlan répondu sous le délai du moteur (10 s), aucun échec de requête ; latence max affichée
testTheSessionMatchesTheContract la session entière (les deux sens) est conforme au contrat OpenRPC embarqué dans le moteur, règles sémantiques comprises (tools/etm-powersync-schema-check --semantic)
testEveryPlanIsAccepted à chaque cycle, le plan est appliqué : l'optimiseur commande, GetPlan sans fallbackCode
testTheSafetyLoopStaysOnTop la boucle de sécurité reste au-dessus : une consigne hors bornes (réécrite en 100 kW par le relais de test) est écrêtée par l'adaptateur (clamped), une surcharge de phase est délestée par L4

Ces deux dernières lignes valent pour le mode plan (défaut). Voir Optimiseur qui s'abstient.

Les fonctions 2 à 5 passent par un relais transparent (une ligne reçue = une ligne renvoyée) qui capture la session et mesure les délais ; la 5 est la seule où il réécrit une réponse. Banc : deux charges etmvariableload (0–2 000 W ; plancher 800, 3 000 W) et une PAC SG-Ready (P3 1 500 W, P4 3 000 W) ; surplus de −1 000 à 9 000 W.

Lancer contre un autre optimiseur

Construire le moteur avec ses tests (qmake6 …/etm-powersync-energy-plugin-etm.pro CONFIG+=build_tests && make -j4), puis, un processus par fonction (le harnais nymea ne rend pas sa mémoire entre deux tests, AGENTS.md) :

cd <build>/tests/auto/conformance
export NYMEA_PLUGINS_PATH=<build>/tests/mocks/plugins/energymocks:/usr/lib/x86_64-linux-gnu/nymea/plugins
export ETM_POWERSYNC_OPTIMIZER_CMD='exec /chemin/de/mon-optimiseur --listen {endpoint}'
for fn in testHandshakeIsAccepted testGetPlanIsAnsweredInTime testTheSessionMatchesTheContract \
          testEveryPlanIsAccepted testTheSafetyLoopStaysOnTop; do
    systemd-run --user --scope -q -p MemoryMax=2G \
        bwrap --dev-bind / / --unshare-net --tmpfs /tmp --bind "$PWD/../../.." "$PWD/../../.." \
              --bind <sources> <sources> --chdir "$PWD" ./target_wrapper.sh ./etm-powersync-optimizer-conformance "$fn"
done

bwrap --unshare-net est recommandé : le harnais ouvre des ports fixes, et l'optimiseur que lance la commande vit dans le même réseau privé (la boucle locale y fonctionne, tcp://127.0.0.1:… compris).

Variable Défaut Sens
ETM_POWERSYNC_OPTIMIZER_CMD l'optimiseur de référence du dépôt, mode normal Commande qui démarre le serveur (/bin/sh -c, dans son propre groupe de processus, arrêtée par SIGTERM puis SIGKILL). {endpoint} y est remplacé par l'endpoint où écouter, aussi passé dans ETM_POWERSYNC_OPTIMIZER_ENDPOINT. - : ne rien lancer, l'optimiseur écoute déjà
ETM_POWERSYNC_OPTIMIZER_ENDPOINT unix://<répertoire temporaire du test>/optimiseur.sock Où l'optimiseur écoute (unix:///… ou tcp://hôte:port)
ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE aucun Fichier du token que l'optimiseur attend ; le moteur l'envoie au handshake (16 caractères au moins)
ETM_POWERSYNC_OPTIMIZER_START_S 15 Délai pour que l'endpoint accepte une connexion
ETM_POWERSYNC_SOURCE_DIR sources du build Racine où trouver le validateur et l'optimiseur de référence
ETM_POWERSYNC_CONFORMANCE_EXPECT plan Ce que la suite attend : plan (l'optimiseur commande) ou abstain (il s'abstient à chaque cycle). Toute autre valeur fait échouer

L'attente de démarrage ouvre une connexion de sonde, refermée sans handshake : un serveur conforme la tolère. Un échec dit ce qui manque — code d'erreur du client, écart au contrat (chemin, mot-clé), cycles non appliqués avec leur fallbackCode, latences.

Optimiseur qui s'abstient

S'abstenir est une réponse permise ({"abstain": true}, §7) : un optimiseur qui s'abstient toujours est conforme. Avec ETM_POWERSYNC_CONFORMANCE_EXPECT=abstain :

  • testEveryPlanIsAccepted exige que chaque cycle (hors le premier, sans réponse encore) finisse en strategy: rules avec fallbackCode: abstain, que chaque réponse soit jugée abstention/abstain, sans échec compté, et qu'optimizerAlive ne change pas. Un plan appliqué, un autre fallbackCode ou un échec font échouer ;
  • testTheSafetyLoopStaysOnTop vérifie toujours L4 sous le rule-based, sans consigne de l'optimiseur : la trace doit porter shed (la seule baisse des consignes ne suffit pas, l'import la produit déjà). Puis la fonction se termine en SKIP : aucune consigne à écrêter ;
  • les trois autres fonctions sont inchangées.

À l'inverse, un optimiseur qui s'abstient en mode plan échoue à testEveryPlanIsAccepted et testTheSafetyLoopStaysOnTop. Chaque fonction affiche en tête le mode retenu ([Conformité] mode attendu : …).

Le mode ne se déduit pas du handshake. Le protocole n'a pas de capacité qui dise « je planifie » : planning est informatif et distingue le plan multi-créneaux du plan à un créneau (§3), et capabilities.kinds absent veut dire « tous les kinds admis », pas « aucun ». L'optimiseur minimal de l'annexe A répond planning: false, sans capabilities, et planifie ; l'optimiseur de référence en mode abstain annonce kinds: ["setpoint", "state"] et ne planifie jamais. Une règle fondée sur ces champs jugerait faux dans les deux cas : le mode se donne, il ne se devine pas.

Ce qu'elle ne vérifie pas

La qualité des plans (aucune comparaison au rule-based : c'est refoptimizer, pour la référence seulement), les bornes VE (aucune borne au banc), UpdateLearned (ignoré par le moteur 1.1), le comportement sous défaut de transport. Le contrat : docs/etm-powersync-optimizer-protocol.openrpc.json (prose : docs/OPTIMIZER_PROTOCOL.md, annexe G).