feat(ref-optimizer): --capture de la session et mode wrong-type

--capture écrit chaque message reçu et envoyé (NDJSON), token remplacé par
autant d'astérisques. wrong-type rend un planId numérique, que le moteur
accepte et que le contrat refuse : contre-épreuve de la conformité.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-10-17 21:15:51 +02:00
parent 9322bc8a2f
commit fc8bc1acd3
2 changed files with 38 additions and 4 deletions

View File

@ -61,10 +61,13 @@ Le rejeu compare, cycle par cycle, les actions du créneau courant au plan enreg
2.0) · `drop` (ferme la connexion à chaque `GetPlan`) · `unknown-load` · `forbidden-kind` (kind
`constraint`) · `missing-load` (une charge disponible absente du créneau courant) · `echo-token`
(recopie le token dans `reason` et `planId`) · `protocol-1.0` (optimiseur 1.0 : sans champs 1.1) ·
`deviate` (cascade en ordre inverse : doit diverger).
`deviate` (cascade en ordre inverse : doit diverger) · `wrong-type` (`planId` numérique : le moteur
l'accepte, le contrat OpenRPC le refuse — contre-épreuve de la suite de conformité).
`--mode-file FICHIER` : le mode est relu dans ce fichier à chaque message — un test change de mode
sans reconnexion. `--list-codes` : le catalogue des codes émissibles qu'il embarque (le test
sans reconnexion. `--capture FICHIER` : chaque message reçu et envoyé, une ligne JSON-RPC par
message — la session que `tools/etm-powersync-schema-check --ndjson` juge contre le contrat ; le
token y devient autant d'astérisques (forme gardée, valeur jamais écrite). `--list-codes` : le catalogue des codes émissibles qu'il embarque (le test
d'intégration le confronte au moteur).
Tests : `tests/auto/refoptimizer` (suite `etm-powersync-refoptimizer`, moteur réel + ce script en

View File

@ -5,7 +5,7 @@
etm-powersync-ref-optimizer [--listen unix:///run/powersync/optimizer.sock | tcp://hôte:port]
[--token-file FICHIER] [--mode MODE | --mode-file FICHIER]
[--slow-s N] [--horizon-s S]
[--slow-s N] [--horizon-s S] [--capture FICHIER]
etm-powersync-ref-optimizer --list-codes
Un serveur JSON-RPC 2.0 sur NDJSON qui répond au `Handshake` et à `GetPlan` par un plan à UN
@ -35,6 +35,12 @@ Modes (`--mode`, pour les tests ; un seul à la fois) :
protocol-1.0 un optimiseur 1.0 : handshake 1.0 sans `capabilities`, plan sans `validUntil`,
`reasonCode`, `reasonParams` ni `diagnostics` (§9 : reste accepté)
deviate cascade servie en ordre de rang INVERSE (contre-épreuve : doit diverger)
wrong-type `planId` numérique au lieu d'une chaîne : le moteur l'ACCEPTE (il le lit comme
texte), le contrat OpenRPC le REFUSE — contre-épreuve de la suite de conformité
`--capture FICHIER` : chaque message reçu et envoyé, une ligne JSON-RPC par message, dans l'ordre
(la session que `tools/etm-powersync-schema-check --ndjson` juge). Le token d'un handshake y est
remplacé par autant d'astérisques : sa FORME reste vérifiable, sa valeur n'est jamais écrite.
`--mode-file` : le mode est relu dans ce fichier à chaque message (un mot ; absent ou illisible =
`normal`). Un test change ainsi de mode SANS reconnexion — ce qui distingue « abstention, optimiseur
@ -78,7 +84,7 @@ LIGNE_MAX = 1024 * 1024 # E-T2-1 : une ligne de plus de 1 Mio est écar
CODE_TOKEN_REFUSE = -32001 # E-T2-6
MODES = ['normal', 'abstain', 'slow', 'invalid', 'expired', 'wrong-major', 'drop', 'unknown-load',
'forbidden-kind', 'missing-load', 'echo-token', 'protocol-1.0', 'deviate']
'forbidden-kind', 'missing-load', 'echo-token', 'protocol-1.0', 'deviate', 'wrong-type']
# Catalogue (energyplugin/etm/types/decisionreason.cpp) : codes qu'un optimiseur peut émettre et
# leurs clés de paramètres documentées (INTERFACE.md). Le test d'intégration les confronte au
@ -341,6 +347,8 @@ def plan(ctx, mode, horizon_s, token):
}
if mode == 'invalid':
resultat['slots'] = 'mode invalid (test) : slots n\'est pas une liste'
elif mode == 'wrong-type':
resultat['planId'] = 42
elif mode == 'protocol-1.0':
# Ce qu'un optimiseur 1.0 savait dire : identifiant, stratégie, créneaux, actions et texte.
for champ in ('validUntil', 'computedAt', 'diagnostics'):
@ -375,10 +383,31 @@ def lire_token(chemin):
return '' # configuré mais illisible : rien ne correspond
VERROU_CAPTURE = threading.Lock()
def capturer(cfg, message):
"""--capture : une ligne par message. Le token reçu au handshake, et toute recopie du token
configuré (mode echo-token), deviennent autant d'astérisques : forme gardée, valeur jamais écrite."""
if not cfg.capture:
return
if isinstance(message, dict) and isinstance(message.get('params'), dict) \
and isinstance(message['params'].get('token'), str):
message = dict(message, params=dict(message['params'], token='*' * len(message['params']['token'])))
ligne = json.dumps(message, separators=(',', ':'), ensure_ascii=False)
token = lire_token(cfg.token_file)
if token:
for forme in (token, json.dumps(token, ensure_ascii=False)[1:-1]):
ligne = ligne.replace(forme, '*' * len(forme))
with VERROU_CAPTURE, open(cfg.capture, 'a', encoding='utf-8') as f:
f.write(ligne + '\n')
class Connexion(socketserver.StreamRequestHandler):
"""Une connexion = un fil ; requêtes traitées dans l'ordre d'arrivée."""
def envoyer(self, message):
capturer(self.server.cfg, message)
ligne = json.dumps(message, separators=(',', ':'), ensure_ascii=False).encode() + b'\n'
self.wfile.write(ligne)
self.wfile.flush()
@ -402,6 +431,7 @@ class Connexion(socketserver.StreamRequestHandler):
except ValueError:
journal('ligne non JSON écartée')
continue
capturer(cfg, message)
if not isinstance(message, dict) or 'method' not in message:
continue
ident, methode = message.get('id'), message.get('method')
@ -497,6 +527,7 @@ def main(argv=None):
p.add_argument('--slow-s', type=float, default=12.0, help='délai du mode slow, en s (défaut 12)')
p.add_argument('--horizon-s', type=int, default=300,
help='durée du créneau et de validUntil, en s (défaut 300)')
p.add_argument('--capture', help='écrit chaque message reçu et envoyé dans ce fichier (NDJSON)')
p.add_argument('--list-codes', action='store_true',
help='écrit le catalogue des codes émissibles (JSON) et sort')
args = p.parse_args(argv)