fix(config): +etm27 refuse deux rangs identiques — l'app le voit avant la box
Livré AVANT le déploiement, parce que ce refus-là est muet : le motif ne franchit pas la frontière RPC, et le symptôme au doigt aurait été « mon glissement ne s'enregistre pas », sans cause visible à l'écran. Même famille que le §9 du contrat de données. Trois points vérifiés, dans l'ordre demandé : 1. LE RÉAPLATISSAGE REND TOUJOURS DES RANGS DISTINCTS. flattenToPayload numérote 1..n sur l'ordre courant, donc par construction — mais « par construction » ne se vérifie pas tout seul : le test le refait après CHAQUE permutation de domaines et après un déplacement intra-domaine, en partant de la config du banc qui porte déjà deux charges au rang 1. Les états intermédiaires du glissement ne partent pas : seul `save()` écrit. 2. RIEN NE PART SI LE BROUILLON N'EST PAS COHÉRENT. validateSet() — le miroir client de LoadConfigStore::validateSet() — apprend le contrôle des rangs et rend un conflit `tiedPriority` qui NOMME les deux charges et le rang partagé. L'écran distingue désormais les trois refus (thing revendiqué deux fois, identifiant en double, rangs à égalité) là où il annonçait « deux charges revendiquent le même appareil » pour tout. 3. ⚠️ LE CHEMIN DE SUPPRESSION ÉTAIT LE PLUS EXPOSÉ, et rien ne l'aurait montré. Il réécrit les survivantes VERBATIM — c'est sa règle, et elle est bonne — donc il transporte les rangs tels que la box les porte. Or c'est le plugin lui-même qui a créé des égalités avant +etm27 (borne détectée au rang 1 d'une installation déjà réglée). Une suppression légitime aurait donc été refusée en bloc, sans motif. Elle refuse maintenant côté app, avec la raison — plutôt que de renumérer en douce : un geste de nettoyage qui redistribue les rangs devient un geste d'arbitrage. Deux tests de load_priority_test construisaient leurs entrées sans `priority` : les deux tombaient donc à égalité, et le nouveau contrôle masquait la règle qu'ils éprouvaient. Rangs distincts posés, avec la raison en commentaire. Les deux autres nouveautés d'+etm27 sont au TODO, non codées : `o:rankOrigin` (badge « rang par défaut, pas un choix », avec la règle absent = rien d'affirmé) et l'entrée en queue des bornes auto-provisionnées — qui devrait faire taire d'elle-même la note d'ordre entrelacé. 213 tests (+8), flutter analyze inchangé à 27 remarques. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
This commit is contained in:
parent
7f0bf71ad8
commit
2c193077f7
23
TODO.md
23
TODO.md
@ -2,6 +2,29 @@
|
||||
|
||||
## Ouvert au 2026-08-27
|
||||
|
||||
- [ ] 🔜 **`1.15.2+etm27` — trois nouveautés, dont une déjà couverte.**
|
||||
Déploiement annoncé pour demain matin.
|
||||
- [x] ~~**Refus en bloc sur deux rangs identiques.**~~ **Couvert le 2026-08-28, avant le
|
||||
déploiement** : `validateSet()` porte le contrôle (`tiedPriority`), l'écran nomme les
|
||||
deux charges et leur rang, et le chemin de suppression — qui réécrit verbatim, donc
|
||||
transporte les rangs de la box — refuse plutôt que d'envoyer une égalité héritée. Huit
|
||||
tests, dont le réaplatissage après **chaque** permutation. Le refus de la box est muet
|
||||
(le motif ne franchit pas la frontière RPC) : sans ce miroir, le symptôme aurait été
|
||||
« mon glissement ne s'enregistre pas ».
|
||||
- [ ] **`o:rankOrigin` — badge « rang par défaut, pas un choix ».** `"auto"` écrit par le
|
||||
moteur quand il crée une entrée d'office, **effacé dès qu'un rang différent arrive**
|
||||
pour cet id. **Absent = rien d'affirmé**, jamais « choisi par un humain » — même règle
|
||||
que `measurement.source`. `"user"` est ce qu'un client écrit pour dire « j'ai vu ce
|
||||
défaut et il me va » sans déplacer la charge ; l'app ne peut **jamais** écrire `"auto"`,
|
||||
le moteur le refuse. C'est l'équivalent, pour une charge arrivée seule, de la note de
|
||||
rangs ex æquo — et il faudra décider si le badge remplace cette note ou s'y ajoute.
|
||||
- [ ] **Les bornes auto-provisionnées entrent en QUEUE (`1 + max`)**, plus en tête. Une
|
||||
borne détectée après la mise en service ne passe donc plus devant l'ECS classé exprès ;
|
||||
sur une installation neuve, la liste étant vide, le VE garde le rang 1. Vérifier que la
|
||||
note d'ordre entrelacé et celle de rangs ex æquo se taisent bien dans ce nouveau cas —
|
||||
elles décrivaient un défaut qui disparaît.
|
||||
|
||||
|
||||
- [x] ~~⏳ Retirer l'alias `measuredW`~~ — **fait le 2026-08-28**, `1.15.2+etm26` déployé
|
||||
sur `.75` et vérifié par sonde : `measuredW` a disparu de la charge utile, `measurement`
|
||||
est en ligne. `MeasurementSource.legacy` est parti avec lui ; ce qui reste,
|
||||
|
||||
@ -364,6 +364,26 @@ class LoadConfigProvider extends ChangeNotifier {
|
||||
final reste = payloadWithoutLoad(frais, loadId);
|
||||
if (reste == null) return 'NotFound';
|
||||
|
||||
// ⚠️ `1.15.2+etm27` refuse EN BLOC un ensemble où deux charges partagent un rang — et
|
||||
// le motif ne franchit pas la frontière RPC.
|
||||
//
|
||||
// Ce chemin réécrit les survivantes VERBATIM, donc il transporte les rangs tels que la
|
||||
// box les porte. Or c'est le plugin lui-même qui a créé des égalités avant `+etm27`
|
||||
// (borne détectée au rang 1, sur une installation où le rang 1 était pris). Une
|
||||
// suppression parfaitement légitime serait donc refusée, sans cause visible.
|
||||
//
|
||||
// On refuse ICI, avec la raison — plutôt que de renuméroter en douce : un geste de
|
||||
// nettoyage qui redistribue les rangs devient un geste d'arbitrage, et c'est
|
||||
// exactement ce que ce chemin s'interdit.
|
||||
final apres = [for (final m in reste) LoadConfigEntry(m)];
|
||||
if (tiedByPriority(apres).isNotEmpty) {
|
||||
_saveError = 'La configuration porte des rangs à égalité, que la box refuse '
|
||||
'depuis 1.15.2+etm27. Classez les charges avant de retirer celle-ci.';
|
||||
_saveState = LoadSaveState.rejected;
|
||||
notifyListeners();
|
||||
return 'TiedRanks';
|
||||
}
|
||||
|
||||
// 3. RÉÉCRIRE.
|
||||
_expectedSignature = _signatureOfPayload(reste);
|
||||
_conflict = null;
|
||||
|
||||
@ -303,25 +303,43 @@ List<DomainGroup> applyDefaultDomainOrder(List<DomainGroup> groups) {
|
||||
|
||||
/// Conflit détecté par [validateSet], suffisamment décrit pour être rendu en français.
|
||||
class LoadSetConflict {
|
||||
/// `duplicateId` ou `thingClaimedTwice`.
|
||||
/// `duplicateId`, `thingClaimedTwice` ou `tiedPriority`.
|
||||
final String kind;
|
||||
final String? id;
|
||||
final String? thingId;
|
||||
final String? firstLabel;
|
||||
final String? secondLabel;
|
||||
|
||||
/// Rang partagé, pour `tiedPriority`.
|
||||
final int? rank;
|
||||
|
||||
const LoadSetConflict.duplicateId(this.id)
|
||||
: kind = 'duplicateId',
|
||||
thingId = null,
|
||||
firstLabel = null,
|
||||
secondLabel = null;
|
||||
secondLabel = null,
|
||||
rank = null;
|
||||
|
||||
const LoadSetConflict.thingClaimedTwice({
|
||||
required this.thingId,
|
||||
required this.firstLabel,
|
||||
required this.secondLabel,
|
||||
}) : kind = 'thingClaimedTwice',
|
||||
id = null;
|
||||
id = null,
|
||||
rank = null;
|
||||
|
||||
/// **Deux charges au même rang** — refusé en bloc depuis `1.15.2+etm27`.
|
||||
///
|
||||
/// Le motif du refus ne franchit pas la frontière RPC : sans ce miroir, le symptôme est
|
||||
/// « mon glissement ne s'enregistre pas », sans cause visible. C'est exactement la
|
||||
/// famille de panne muette que ce dépôt traque.
|
||||
const LoadSetConflict.tiedPriority({
|
||||
required this.rank,
|
||||
required this.firstLabel,
|
||||
required this.secondLabel,
|
||||
}) : kind = 'tiedPriority',
|
||||
id = null,
|
||||
thingId = null;
|
||||
}
|
||||
|
||||
/// Rejoue la validation d'ensemble du plugin **avant** l'envoi.
|
||||
@ -329,17 +347,32 @@ class LoadSetConflict {
|
||||
/// Miroir de `LoadConfigStore::validateSet()` (`loadconfigstore.cpp:83-112`) :
|
||||
/// unicité des identifiants sur **toutes** les charges, unicité des Things revendiqués
|
||||
/// **parmi les `enabled` seulement** — une charge désactivée ne construit aucun
|
||||
/// adaptateur, donc ne revendique rien.
|
||||
/// adaptateur, donc ne revendique rien — et, depuis `1.15.2+etm27`, **unicité des rangs**.
|
||||
///
|
||||
/// Ce troisième contrôle vaut plus que les deux autres, parce que le refus qu'il anticipe
|
||||
/// est **muet** : le motif ne franchit pas la frontière RPC, et le symptôme au doigt est
|
||||
/// « mon glissement ne s'enregistre pas ».
|
||||
///
|
||||
/// C'est un **confort d'affichage**. La box reste l'autorité : si elle refuse, c'est son
|
||||
/// refus qu'il faut montrer.
|
||||
LoadSetConflict? validateSet(List<LoadConfigEntry> entries) {
|
||||
final ids = <String>{};
|
||||
final owner = <String, String>{}; // thingId normalisé → libellé de la charge
|
||||
final rangs = <int, String>{}; // priority → libellé de la première charge vue
|
||||
|
||||
for (final c in entries) {
|
||||
if (!ids.add(c.id)) return LoadSetConflict.duplicateId(c.id);
|
||||
|
||||
// ⚠️ `1.15.2+etm27` : deux charges au même rang font refuser TOUT l'ensemble. Le
|
||||
// contrôle porte sur ce qui part, y compris une charge désactivée — le moteur ne les
|
||||
// exclut pas de cette règle-ci, contrairement à la revendication de Thing.
|
||||
final vu = rangs[c.priority];
|
||||
if (vu != null) {
|
||||
return LoadSetConflict.tiedPriority(
|
||||
rank: c.priority, firstLabel: vu, secondLabel: c.label);
|
||||
}
|
||||
rangs[c.priority] = c.label;
|
||||
|
||||
if (!c.enabled) continue; // ne construit aucun adaptateur : ne revendique rien
|
||||
|
||||
for (final thing in c.claimedThingIds) {
|
||||
|
||||
@ -903,9 +903,14 @@ class _RejectionNote extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
conflit != null
|
||||
? 'Deux charges revendiquent le même appareil.'
|
||||
: 'La box a refusé l\'enregistrement${err == null ? '' : ' ($err)'}.',
|
||||
switch (conflit?.kind) {
|
||||
'thingClaimedTwice' => 'Deux charges revendiquent le même appareil.',
|
||||
'duplicateId' => 'Deux charges portent le même identifiant.',
|
||||
'tiedPriority' =>
|
||||
'Deux charges se retrouvent au rang ${conflit!.rank} : '
|
||||
'${conflit.firstLabel} et ${conflit.secondLabel}.',
|
||||
_ => 'La box a refusé l\'enregistrement${err == null ? '' : ' ($err)'}.',
|
||||
},
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
@ -913,15 +918,27 @@ class _RejectionNote extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
conflit != null
|
||||
// LM-201 : deux charges sur un canal unique se contrediraient, et le
|
||||
// budget compterait la même puissance deux fois. Ce n'est pas une
|
||||
// limite technique — c'est le modèle.
|
||||
? 'Un appareil ne peut appartenir qu\'à une seule charge : deux '
|
||||
'charges sur un même canal de commande se contrediraient.'
|
||||
: 'Le refus porte sur l\'ensemble : rien n\'a été modifié. La règle '
|
||||
'enfreinte et le libellé en cause sont dans le journal de la box '
|
||||
'(journalctl -u nymead).',
|
||||
switch (conflit?.kind) {
|
||||
// LM-201 : deux charges sur un canal unique se contrediraient, et le
|
||||
// budget compterait la même puissance deux fois. Ce n'est pas une
|
||||
// limite technique — c'est le modèle.
|
||||
'thingClaimedTwice' =>
|
||||
'Un appareil ne peut appartenir qu\'à une seule charge : deux '
|
||||
'charges sur un même canal de commande se contrediraient.',
|
||||
'duplicateId' =>
|
||||
'Chaque charge a un identifiant unique côté box — deux entrées ne '
|
||||
'peuvent pas le partager.',
|
||||
// Depuis +etm27 la box refuse TOUT l'ensemble sur une égalité de rang, et
|
||||
// son motif ne franchit pas la frontière RPC : sans ce message, le
|
||||
// symptôme serait « ça ne s'enregistre pas », sans cause visible.
|
||||
'tiedPriority' =>
|
||||
'Depuis la version 1.15.2+etm27, la box refuse toute configuration où '
|
||||
'deux charges partagent un rang. Classez-les l\'une après l\'autre '
|
||||
'— rien n\'a été envoyé.',
|
||||
_ => 'Le refus porte sur l\'ensemble : rien n\'a été modifié. La règle '
|
||||
'enfreinte et le libellé en cause sont dans le journal de la box '
|
||||
'(journalctl -u nymead).',
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
height: 1.35,
|
||||
|
||||
@ -477,6 +477,9 @@ class _BoutonRetirerState extends State<_BoutonRetirer> {
|
||||
'NotFound' =>
|
||||
'Cette charge n\'est plus dans la configuration de la box — rien à retirer.',
|
||||
'NoService' => 'Aucune connexion à la box.',
|
||||
'TiedRanks' =>
|
||||
'Deux charges partagent un rang, et la box refuse alors toute la configuration '
|
||||
'(1.15.2+etm27). Classez-les d\'abord — rien n\'a été envoyé.',
|
||||
'RereadFailed' =>
|
||||
'Suppression envoyée, mais impossible de relire la configuration pour le '
|
||||
'vérifier. Rouvrez l\'écran avant tout autre geste.',
|
||||
|
||||
@ -197,8 +197,10 @@ void main() {
|
||||
test('un Thing commandé par deux charges ACTIVES est refusé', () {
|
||||
final relay = [{'thingId': '{8538782f-2c8c-4a30-bfce-c8140f791c9b}', 'powerW': 500}];
|
||||
final c = validateSet([
|
||||
LoadConfigEntry({'id': 'a', 'label': 'A', 'adapter': 'relay-router', 'enabled': true, 'relays': relay}),
|
||||
LoadConfigEntry({'id': 'b', 'label': 'B', 'adapter': 'relay-router', 'enabled': true, 'relays': relay}),
|
||||
// Rangs DISTINCTS : depuis +etm27 une égalité fait refuser l'ensemble, et le
|
||||
// miroir la voit en premier — elle masquerait la règle éprouvée ici.
|
||||
LoadConfigEntry({'id': 'a', 'label': 'A', 'priority': 1, 'adapter': 'relay-router', 'enabled': true, 'relays': relay}),
|
||||
LoadConfigEntry({'id': 'b', 'label': 'B', 'priority': 2, 'adapter': 'relay-router', 'enabled': true, 'relays': relay}),
|
||||
]);
|
||||
expect(c?.kind, 'thingClaimedTwice');
|
||||
expect(c?.firstLabel, 'A');
|
||||
@ -208,8 +210,8 @@ void main() {
|
||||
test('une charge DÉSACTIVÉE ne revendique rien — pas de conflit', () {
|
||||
final relay = [{'thingId': '{8538782f-2c8c-4a30-bfce-c8140f791c9b}', 'powerW': 500}];
|
||||
final c = validateSet([
|
||||
LoadConfigEntry({'id': 'a', 'label': 'A', 'adapter': 'relay-router', 'enabled': true, 'relays': relay}),
|
||||
LoadConfigEntry({'id': 'b', 'label': 'B', 'adapter': 'relay-router', 'enabled': false, 'relays': relay}),
|
||||
LoadConfigEntry({'id': 'a', 'label': 'A', 'priority': 1, 'adapter': 'relay-router', 'enabled': true, 'relays': relay}),
|
||||
LoadConfigEntry({'id': 'b', 'label': 'B', 'priority': 2, 'adapter': 'relay-router', 'enabled': false, 'relays': relay}),
|
||||
]);
|
||||
expect(c, isNull);
|
||||
});
|
||||
|
||||
151
test/rangs_distincts_test.dart
Normal file
151
test/rangs_distincts_test.dart
Normal file
@ -0,0 +1,151 @@
|
||||
/// **`1.15.2+etm27` refuse en bloc un ensemble où deux charges partagent un rang.**
|
||||
///
|
||||
/// Le motif du refus ne franchit pas la frontière RPC : le symptôme au doigt serait « mon
|
||||
/// glissement ne s'enregistre pas », sans cause visible. Ces tests vérifient les trois
|
||||
/// choses que ça exige de l'app, **avant** le déploiement :
|
||||
///
|
||||
/// 1. le réaplatissage produit toujours des rangs distincts, quel que soit l'état d'où il
|
||||
/// part — y compris un ordre entrelacé ou déjà à égalité ;
|
||||
/// 2. rien ne part à la box tant que le brouillon n'est pas cohérent — le miroir client
|
||||
/// voit l'égalité avant l'envoi ;
|
||||
/// 3. le chemin de suppression, qui réécrit VERBATIM, refuse plutôt que de transporter une
|
||||
/// égalité que la box rejettera.
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:etm_powersync_app/models/load_config_entry.dart';
|
||||
import 'package:etm_powersync_app/services/load_priority.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
List<LoadConfigEntry> _banc([String fixture = 'loadconfig_hems75_v2c.json']) {
|
||||
final raw = jsonDecode(File('test/fixtures/$fixture').readAsStringSync());
|
||||
return [
|
||||
for (final m in ((raw as Map<String, dynamic>)['loadConfigs'] as List))
|
||||
LoadConfigEntry(Map<String, dynamic>.from(m as Map)),
|
||||
];
|
||||
}
|
||||
|
||||
/// Les rangs d'une charge utile prête à partir.
|
||||
List<int> _rangs(List<Map<String, dynamic>> payload) =>
|
||||
[for (final m in payload) m['priority'] as int];
|
||||
|
||||
void main() {
|
||||
group('le réaplatissage rend TOUJOURS des rangs distincts', () {
|
||||
test('sur le banc, dont l\'ordre porte deux charges au rang 1', () {
|
||||
// C'est l'état réel du 2026-08-28 : la V2C créée d'office au rang 1 que le
|
||||
// chauffe-eau occupait déjà. Sous +etm27, réécrire cet ensemble tel quel serait
|
||||
// refusé — le réaplatissage doit donc le réparer, et il le fait.
|
||||
final entrees = _banc();
|
||||
expect(tiedByPriority(entrees), isNotEmpty, reason: 'prémisse du test');
|
||||
|
||||
final rangs = _rangs(flattenToPayload(groupByDomain(entrees).groups));
|
||||
expect(rangs.toSet().length, rangs.length);
|
||||
expect(rangs, List.generate(entrees.length, (i) => i + 1));
|
||||
});
|
||||
|
||||
test('sur un ordre entrelacé', () {
|
||||
final rangs = _rangs(
|
||||
flattenToPayload(groupByDomain(_banc('loadconfig_interleaved.json')).groups));
|
||||
expect(rangs.toSet().length, rangs.length);
|
||||
});
|
||||
|
||||
test('après CHAQUE déplacement, pas seulement à la fin', () {
|
||||
// Le glissement produit des états intermédiaires : ce qui compte est qu'aucun d'eux
|
||||
// ne puisse partir avec une égalité, pas seulement l'état final.
|
||||
var groupes = groupByDomain(_banc()).groups;
|
||||
for (var i = 0; i < groupes.length; i++) {
|
||||
for (var j = 0; j < groupes.length; j++) {
|
||||
final permute = [...groupes];
|
||||
permute.insert(j, permute.removeAt(i));
|
||||
final rangs = _rangs(flattenToPayload(permute));
|
||||
expect(rangs.toSet().length, rangs.length,
|
||||
reason: 'domaine $i → $j produit une égalité');
|
||||
}
|
||||
}
|
||||
// Et à l'intérieur d'un domaine.
|
||||
groupes = groupByDomain(_banc()).groups;
|
||||
final ev = groupes.indexWhere((g) => g.domain == 'ev');
|
||||
if (ev >= 0 && groupes[ev].loads.length > 1) {
|
||||
final charges = [...groupes[ev].loads];
|
||||
charges.insert(0, charges.removeLast());
|
||||
final refait = [
|
||||
for (final (i, g) in groupes.indexed)
|
||||
if (i == ev) DomainGroup(domain: g.domain, loads: charges) else g
|
||||
];
|
||||
final rangs = _rangs(flattenToPayload(refait));
|
||||
expect(rangs.toSet().length, rangs.length);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('rien ne part si le brouillon n\'est pas cohérent', () {
|
||||
test('le miroir client VOIT l\'égalité de rang', () {
|
||||
// Sans ce contrôle, la box refuserait tout l'ensemble sans dire pourquoi.
|
||||
final conflit = validateSet(_banc());
|
||||
expect(conflit, isNotNull);
|
||||
expect(conflit!.kind, 'tiedPriority');
|
||||
expect(conflit.rank, 1);
|
||||
expect([conflit.firstLabel, conflit.secondLabel],
|
||||
containsAll(['chauffe-eau', 'V2C Trydan']));
|
||||
});
|
||||
|
||||
test('un ensemble réaplati passe le miroir', () {
|
||||
// Le chemin normal de l'écran : on réaplatit, donc on n'envoie jamais d'égalité.
|
||||
final aplati = [
|
||||
for (final m in flattenToPayload(groupByDomain(_banc()).groups))
|
||||
LoadConfigEntry(m)
|
||||
];
|
||||
expect(validateSet(aplati), isNull);
|
||||
});
|
||||
|
||||
test('l\'égalité est vue AVANT les autres conflits qu\'elle masquerait', () {
|
||||
// Une charge désactivée compte aussi : le moteur ne l'exclut pas de cette règle-ci,
|
||||
// contrairement à la revendication de Thing.
|
||||
final entrees = [
|
||||
LoadConfigEntry(const {
|
||||
'id': 'a',
|
||||
'label': 'A',
|
||||
'priority': 2,
|
||||
'adapter': 'evcharger',
|
||||
'enabled': true
|
||||
}),
|
||||
LoadConfigEntry(const {
|
||||
'id': 'b',
|
||||
'label': 'B',
|
||||
'priority': 2,
|
||||
'adapter': 'evcharger',
|
||||
'enabled': false
|
||||
}),
|
||||
];
|
||||
expect(validateSet(entrees)?.kind, 'tiedPriority');
|
||||
});
|
||||
});
|
||||
|
||||
group('la suppression ne transporte pas une égalité', () {
|
||||
test('retirer une charge NE RÉPARE PAS les rangs — et c\'est voulu', () {
|
||||
// Le chemin de suppression réécrit verbatim : les rangs survivent tels quels. C'est
|
||||
// ce qui rend le garde-fou nécessaire, et c'est aussi ce qui empêche un geste de
|
||||
// nettoyage de devenir un geste d'arbitrage.
|
||||
final frais = [
|
||||
for (final e in _banc()) Map<String, dynamic>.from(e.raw)
|
||||
];
|
||||
final reste = payloadWithoutLoad(
|
||||
frais, '{88160e45-7df2-48cc-9639-f0d15ae6e382}')!;
|
||||
final apres = [for (final m in reste) LoadConfigEntry(m)];
|
||||
expect(tiedByPriority(apres), isNotEmpty,
|
||||
reason: 'l\'égalité chauffe-eau / V2C survit à la suppression — d\'où le refus');
|
||||
});
|
||||
|
||||
test('quand la charge retirée PORTAIT l\'égalité, la suppression la résout', () {
|
||||
final frais = [
|
||||
{'id': 'a', 'label': 'A', 'priority': 1, 'adapter': 'relay-router'},
|
||||
{'id': 'b', 'label': 'B', 'priority': 1, 'adapter': 'evcharger'},
|
||||
{'id': 'c', 'label': 'C', 'priority': 2, 'adapter': 'sg-ready'},
|
||||
];
|
||||
final reste = payloadWithoutLoad(frais, 'b')!;
|
||||
expect(tiedByPriority([for (final m in reste) LoadConfigEntry(m)]), isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user