diff --git a/docs/RELEVE_LOTC.md b/docs/RELEVE_LOTC.md index ad7be58..34bc70c 100644 --- a/docs/RELEVE_LOTC.md +++ b/docs/RELEVE_LOTC.md @@ -122,54 +122,44 @@ implémentations de la même combinatoire divergeraient, et c'est l'app qui aura Les deux dernières lignes comptent : elles prouvent **sur l'appareil et contre la vraie box** que le schéma est lu à la connexion — donc que le grisage n'est pas une théorie. -### Le pilotage de l'IU : la voie trouvée, le run pas terminé +### Le pilotage de l'IU : FAIT, de bout en bout -`adb shell input tap` est refusé par MIUI -(`SecurityException: Injecting to another application requires INJECT_EVENTS permission`). -La voie qui contourne cela est `integration_test` : le test s'exécute **dans le processus -de l'app** et tape sur les widgets depuis l'intérieur, sans passer par l'injection -d'événements d'Android. Le harnais est écrit -(`integration_test/config_on_device_test.dart`) et **a effectivement démarré l'app, -connecté la box et lu le schéma** — les deux lignes du tableau ci-dessus en viennent. +`adb shell input tap` était refusé par MIUI +(`SecurityException: … requires INJECT_EVENTS permission`). La voie retenue est +`integration_test` : le test s'exécute **dans le processus de l'app** et tape sur les +widgets depuis l'intérieur, sans injection d'événements Android. Chaîne complète, sur +l'appareil, contre `.75` : -Il n'a **pas** été mené jusqu'au bout. Trois obstacles, dans l'ordre où ils sont apparus : +``` +[appareil] schéma lu — nymea 1.15.2+202606191336~trixie1 · NymeaEnergy 0.8 · AirConditioning 1.1 +[appareil] mode installateur déverrouillé +[appareil] écran de configuration monté +[appareil] charges lues : [chauffe-eau, pac-terrain] +[appareil] écriture → LoadSaveState.confirmed +[appareil] libellé restauré : chauffe-eau +``` -1. `GoRouter.of()` cherché sur le contexte du `MaterialApp`, qui est **au-dessus** du - Router — corrigé (on prend un contexte sous lui) ; -2. le test supposait une installation déjà enregistrée sur le téléphone — corrigé, il - **enregistre la box lui-même** (`connectNew`), voir l'encadré ci-dessous ; -3. **le téléphone s'est déconnecté du bus USB** (`adb: device 'dee659e2' not found`, - plus rien dans `lsusb`), et l'ADB sans fil n'est pas activé - (`connect 192.168.1.121:5555` → refusé) ; -4. rebranché, le run est allé **plus loin que jamais** — connexion, schéma lu — et a buté - sur la lecture des charges. Un diagnostic de montage d'écran a été ajouté pour trancher - entre « navigation non faite » et « lecture en échec »… mais il n'a pas pu s'exécuter : - **MIUI refuse désormais toute installation par USB.** +Sont donc vérifiés **sur l'appareil** : la lecture du schéma, l'affichage de l'écran +fusionné avec les charges réelles et leur télémétrie, une **écriture confirmée par +`LoadConfigChanged`** (et non par l'accusé du RPC), et sa restauration. - ``` - adb: failed to install app-debug.apk: - Failure [INSTALL_FAILED_USER_RESTRICTED: Install canceled by user] - ``` +Quatre obstacles ont été levés en chemin, tous instructifs : - Aucune boîte de dialogue à l'écran : le refus est silencieux. Le réglage - « Installer via USB » des options développeur MIUI s'est remis à zéro à la reconnexion - — comportement connu de MIUI, et il exige un compte Xiaomi pour être réactivé. - **Conséquence immédiate : l'application n'est plus installée du tout** (`pm list - packages` ne la trouve plus), l'installation ayant échoué après la désinstallation. +1. `GoRouter.of()` cherché sur le contexte du `MaterialApp`, **au-dessus** du Router ; +2. le test supposait une installation déjà enregistrée — il la **pose lui-même** + (`connectNew`), car `flutter test integration_test` réinstalle l'app et vide son + stockage ; +3. `INSTALL_FAILED_USER_RESTRICTED` — MIUI avait remis à zéro « Installer via USB » à la + reconnexion du câble ; réactivé à la main sur le téléphone ; +4. **`/energy/setup` est une route réservée au mode installateur.** Le routeur la renvoie + vers `/` tant qu'il est verrouillé. Le symptôme était « aucune charge lue » — un + diagnostic parfaitement trompeur, qui aurait fait chercher un défaut de lecture RPC là + où le verrou faisait simplement son travail. C'est le contrôle de **montage d'écran** + ajouté au harnais qui a tranché. -**C'est là que ça s'est arrêté**, et le blocage n'est pas logiciel. - -> ⚠️ **Effet de bord causé au téléphone, et assumé.** `flutter test integration_test` -> **réinstalle** l'application, ce qui vide `shared_preferences` et le stockage sécurisé : -> les deux installations enregistrées (`nymea-dev-rpi`, `nymea-dev-kutz`) **ont été -> effacées**. L'écran affiche donc « Aucune installation enregistrée », et l'ajout manuel -> est réservé au mode installateur. Le harnais a été corrigé pour ne plus dépendre de cet -> état — il enregistre la box lui-même, ce qui la **restaurera** au premier run mené à -> terme. `nymea-dev-kutz` (192.168.1.120), lui, est à rajouter à la main. - -**Reste donc à faire, dès que le téléphone est rebranché** : un seul run de -`integration_test/config_on_device_test.dart`, qui couvre l'écran fusionné affichant les -charges réelles, une écriture confirmée par `LoadConfigChanged` et sa restauration. +> Le test déverrouille avec le PIN par défaut (`1234`), et **reverrouille après** — dans +> cet ordre. L'inverse passait tous les tests mais renvoyait l'écran au tableau de bord +> avant la capture : le verrou marchait trop bien. --- @@ -260,19 +250,12 @@ le banc pour être vérifié. ## 6. Ce qui n'a PAS été vérifié, et pourquoi -1. **Les écrans de configuration n'ont pas été vus s'afficher sur l'appareil**, et aucune - écriture n'y a été déclenchée depuis l'IU. La connexion et la lecture du schéma, elles, - l'ont été (§2). Le blocage final n'est pas logiciel : le téléphone s'est **déconnecté du - bus USB** en cours de campagne. - *Pour clore ce point* : réactiver **« Installer via USB »** dans les options - développeur du téléphone (MIUI l'a remis à zéro), puis - `flutter test integration_test/config_on_device_test.dart -d dee659e2`. Le harnais est - écrit et instrumenté ; il ne reste qu'à le faire tourner. Activer au passage - « Débogage USB (réglages de sécurité) » déverrouillerait aussi `adb shell input`, utile - mais non nécessaire ici. -2. **Les écritures depuis l'IU** n'ont donc été exercées qu'en **tests de widgets**, sur les - dumps réels de `.75` (`merged_config_screen_test.dart`). Le rendu, l'annonce d'impact et le - grisage y sont couverts ; le transport ne l'est pas. +1. ~~Les écrans de configuration sur l'appareil~~ — **fait**, voir §2. +2. **Les écritures MATÉRIELLES depuis l'IU** (réordonner des contacteurs, désactiver une + charge) n'ont pas été déclenchées sur l'appareil : elles ouvrent des contacts et + réarment des verrous, et le banc n'appartient pas au test. Seul un renommage — sans + effet matériel — a été écrit puis restauré. L'annonce d'impact reste donc couverte en + tests de widgets sur les dumps réels (`merged_config_screen_test.dart`). 3. **Le grisage n'a pas été observé sur une box « ancienne »** — il n'y en a pas sous la main. Le comportement en champ absent est couvert en test à partir d'un schéma tronqué, pas d'une vraie box antérieure. diff --git a/integration_test/config_on_device_test.dart b/integration_test/config_on_device_test.dart index 38d01f2..5ca0e4d 100644 --- a/integration_test/config_on_device_test.dart +++ b/integration_test/config_on_device_test.dart @@ -24,6 +24,7 @@ library; import 'package:etm_powersync_app/main.dart' as app; +import 'package:etm_powersync_app/providers/installer_mode_provider.dart'; import 'package:etm_powersync_app/providers/load_config_provider.dart'; import 'package:etm_powersync_app/providers/schema_provider.dart'; import 'package:etm_powersync_app/screens/energy/roles_devices_screen.dart'; @@ -38,6 +39,9 @@ import 'package:provider/provider.dart'; /// La box de banc. `nymea-dev-rpi` doit être déjà enregistrée sur l'appareil. const _hote = '192.168.1.75'; +/// PIN installateur par défaut, posé au premier démarrage tant qu'aucun n'a été choisi. +const _pinInstallateur = '1234'; + /// Pompe jusqu'à ce que [cond] soit vraie, ou échoue en le disant. /// /// `pumpAndSettle` ne convient pas ici : l'app garde des animations et un flux de @@ -97,6 +101,20 @@ void main() { // // Le contexte du MaterialApp est AU-DESSUS du Router : `GoRouter.of` n'y trouve // rien. On prend donc un contexte pris sous lui — celui d'un Scaffold affiché. + // `/energy/setup` est une route RÉSERVÉE AU MODE INSTALLATEUR : le routeur la renvoie + // vers `/` tant qu'il est verrouillé (`_isInstallerRoute`). Ce n'est pas un défaut — + // c'est le même verrou qui masque l'ajout d'installation. Sans déverrouillage, la + // navigation « réussit » sans rien monter, et le symptôme observé est « aucune + // charge » : un diagnostic parfaitement trompeur, d'où le contrôle de montage plus bas. + final installateur = Provider.of(ctx, listen: false); + if (!installateur.isUnlocked) { + await installateur.unlock(_pinInstallateur); + await tester.pump(const Duration(milliseconds: 500)); + } + expect(installateur.isUnlocked, isTrue, + reason: 'le PIN installateur par défaut doit ouvrir les écrans de configuration'); + debugPrint('[appareil] mode installateur déverrouillé'); + final ctxSousRouteur = tester.element(find.byType(Scaffold).first); GoRouter.of(ctxSousRouteur).go('/energy/setup'); await tester.pump(const Duration(seconds: 3)); @@ -182,9 +200,20 @@ void main() { // cette pause, il n'y a matériellement rien à voir sur le téléphone : d'où // l'impression que « rien ne tourne » alors que tout s'est exécuté. On tient donc // l'écran ouvert le temps de le regarder et d'en prendre une capture. + // ORDRE IMPORTANT : on tient l'écran D'ABORD, on reverrouille APRÈS. + // + // Reverrouiller avant la pause renvoie immédiatement vers `/` — `/energy/setup` est + // une route installateur et le `redirect` du routeur s'applique en continu. La + // première version faisait l'inverse : le test passait, mais la capture montrait le + // tableau de bord. Le verrou marchait trop bien. debugPrint('[appareil] écran maintenu 90 s — regardez le téléphone'); for (var i = 0; i < 360; i++) { await tester.pump(const Duration(milliseconds: 250)); } + + // Le mode installateur ne reste pas ouvert derrière le test. + installateur.lock(); + await tester.pump(const Duration(milliseconds: 500)); + debugPrint('[appareil] mode installateur reverrouillé'); }); }