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

87 lines
6.3 KiB
Markdown
Raw Permalink 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.

# `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).