/// **Un instant publié par la box se rend chez le LECTEUR** — quelle que soit sa forme. /// /// Trois formes coexistaient à la frontière RPC, et rien ne l'exigeait : epoch, ISO marqué /// `Z`, ISO nu. La troisième est celle qui coûtait, parce qu'elle ne se distingue de la /// deuxième par rien de visible et que Dart la tient pour **locale** — `toLocal()` n'a /// alors aucun effet. /// /// Les deux défauts frères du 2026-09-06, trouvés par la même bascule de `.75` en /// `Europe/London` : /// /// - côté app, `measuredSince` marqué `Z` mais rendu sans `toLocal()` → heure UTC à l'écran ; /// - côté moteur, `command.since` en UTC publié **sans marqueur** → une chaîne qu'un client /// lit comme locale. /// /// Aucun des deux ne se voit quand le **lecteur** est à UTC+0 — et le fuseau de la BOX n'y /// est pour rien : mesuré le 2026-09-07 avec box et téléphone en `Europe/Paris`, l'écran /// affichait encore l'heure UTC. C'est pour ça que ce fichier construit ses attendus par /// calcul plutôt que de les recopier : un littéral d'heure gèle un fuseau. library; import 'package:etm_powersync_app/models/box_instant.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { /// L'instant de référence, choisi hors heure d'été nulle part : 2026-09-06 20:19:04 UTC. final reference = DateTime.utc(2026, 9, 6, 20, 19, 4); test('les trois formes désignent le MÊME instant', () { final epoch = instantDeLaBox(reference.millisecondsSinceEpoch ~/ 1000); final marquee = instantDeLaBox('2026-09-06T20:19:04Z'); final nue = instantDeLaBox('2026-09-06T20:19:04'); expect(epoch, isNotNull); expect(marquee, isNotNull); expect(nue, isNotNull); // On compare des instants absolus : `isAtSameMomentAs` ignore la représentation. expect(marquee!.isAtSameMomentAs(reference), isTrue); expect(epoch!.isAtSameMomentAs(reference), isTrue); expect(nue!.isAtSameMomentAs(reference), isTrue, reason: 'une chaîne sans marqueur est de l\'UTC d\'une box antérieure au contrat, ' 'pas une heure locale'); }); test('le résultat est rendu dans le fuseau du LECTEUR, jamais en UTC', () { final d = instantDeLaBox('2026-09-06T20:19:04Z')!; expect(d.isUtc, isFalse, reason: 'un instant destiné à l\'écran est déjà local'); expect(d.hour, reference.toLocal().hour); // Le contrôle qui MORD — et il ne mord que hors UTC. Sous `TZ=UTC`, local et UTC // coïncident et cette assertion passerait avec le défaut comme sans lui : un vert // obtenu là ne prouve rien, et c'est écrit pour que personne ne le lise comme tel. if (reference.toLocal().hour != reference.hour) { expect(d.hour, isNot(reference.hour)); } }); test('la forme NUE ne se lit pas comme locale — le défaut du moteur', () { // Le défaut consistait exactement en ceci : `DateTime.parse` d'une chaîne nue rend // `isUtc: false`, donc `.hour` vaut 20 quel que soit l'endroit du monde. Ici, 20 h ne // doit sortir que là où l'offset est nul. final nue = instantDeLaBox('2026-09-06T20:19:04')!; final naif = DateTime.parse('2026-09-06T20:19:04').toLocal(); if (reference.toLocal().hour != reference.hour) { expect(nue.hour, isNot(naif.hour), reason: 'la lecture naïve est celle qui décalait silencieusement'); } expect(nue.isAtSameMomentAs(reference), isTrue); }); test('ce qui n\'est pas un instant rend `null`, pas une date par défaut', () { expect(instantDeLaBox(null), isNull); expect(instantDeLaBox(''), isNull); expect(instantDeLaBox('pas une date'), isNull); expect(instantDeLaBox(const {}), isNull); // Un champ absent est un état. Retomber sur l'epoch 0 ou sur `DateTime.now()` en // ferait une mesure — le zéro forgé, appliqué au temps. }); }