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>
87 lines
6.3 KiB
Markdown
87 lines
6.3 KiB
Markdown
# `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`) :
|
||
|
||
```sh
|
||
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).
|