Le contrat porte désormais la règle : tout horodatage publié est en UTC avec son
marqueur, ou en epoch. Trois champs, deux formats, rien ne l'exigeait.
`instantDeLaBox` (lib/models/box_instant.dart) devient l'unique lecteur pour ce
que la box publie — epoch ou ISO, marqué ou nu — et requalifie la forme nue en
UTC avant de rendre l'heure du lecteur. Politique valable pour la BOX seule :
`Installation.lastSeen` est écrite en local sans marqueur et relue pareil, donc
la lire en UTC la décalerait. Deux héritages opposés sous une même forme, d'où
l'absence délibérée d'un « parser une date » unique.
Le `.toLocal()` posé hier dans telemetry_text est retiré : le modèle est seul
propriétaire de la conversion. Le rejouer serait sans effet mais laisserait
croire qu'elle peut manquer ailleurs.
CORRECTION. J'avais écrit que le défaut avait survécu « parce que box et
téléphone partageaient l'heure ». C'est faux, et la fausseté est utile : l'écart
est entre UTC et l'APPAREIL, donc visible sur tout téléphone hors UTC+0 quel que
soit le fuseau de la box. Mesuré ce matin avec les deux en Europe/Paris — la box
publie 04:33:28Z, l'APK d'hier affiche « 04:33 », il fallait lire 06:33. La
bascule en Europe/London n'a pas donné la visibilité, elle a donné le MOTIF de
regarder. La version fausse fait conclure « les fuseaux coïncident, donc c'est
réglé » et laisse le défaut en place.
`GetVariableLoadState` n'existe pas sur .75 (absente d'Introspect) et l'app ne
lit aucun `since` : rien à corriger de ce côté. Le champ existe en revanche dans
GetLoadTelemetry — `loads[].command {divergent, expectedW, observedW, since}` —
et il porte bien le Z, vérifié à la sonde. Aucun écran ne le consomme encore.
Contrôle négatif de la ligne d'horloge : même binaire qu'hier, box repassée en
Europe/Paris, la ligne a disparu sous le champ d'échéance.
docs/captures/2026-09-07_fuseau-box-paris-ligne-absente.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
79 lines
3.8 KiB
Dart
79 lines
3.8 KiB
Dart
/// **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 <String, Object>{}), 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.
|
|
});
|
|
}
|