docs(android): TESTS.md decrit l'application telle qu'elle est

La version precedente decrivait une application qui n'existe plus - deux
secrets confondus, des reglages qui n'appliquaient rien sur-le-champ, un
coffre qui s'ouvrait sans rien demander.

Trois series nouvelles : les deux secrets (C), l'ouverture qui demande
quelque chose (D), et le clavier avec le retour d'application (I). F est
reprise en entier. La remise a zero passe desormais par le bouton Debug
plutot que par adb, ce qui rend le scenario deroulable sans machine.

En tete, ce que la seance a vraiment appris : les defauts se tiennent par
la main, et le journal doit se lire AVANT de retenter - la
synchronisation le vide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-28 21:52:43 +01:00
parent 85691bb2f1
commit eb49a4ab50
+181 -113
View File
@@ -1,60 +1,76 @@
# Client Android — relecture et scénario d'essai # Client Android — relecture et scénario d'essai
Relecture du 2026-08-22, avant d'ajouter la vérification de fond, les réglages Réécrit le **2026-08-28**, après une séance qui a remué les deux secrets du
et les icônes. Le but n'était pas de tout relire mais de chercher là où cette coffre, l'écran des réglages, le code de déverrouillage et le clavier. La
séance a montré que les défauts se logent : **les frontières entre fichiers, version précédente décrivait une application qui n'existe plus.
les chemins d'erreur, et ce qui traverse les threads.**
Ce que cette séance a confirmé, et qui vaut plus que n'importe quelle liste :
**les défauts se tiennent par la main.** Corriger le clavier qui volait le
premier appui a laissé le clavier ouvert ; le clavier ouvert recouvrait le
message ; et le message invisible cachait qu'une action avait réussi. Aucun des
deux derniers n'était observable tant que le premier tenait. D'où la règle qui
suit : **quand un essai échoue, lire le journal AVANT de retenter**, parce que la
synchronisation le vide.
--- ---
## Ce que la relecture a trouvé ## Ce que la séance a corrigé, et pourquoi c'est éprouvable
### 1. La fusion ne transportait pas les dossiers — CORRIGÉ ### 1. Le journal ne perdait plus les causes — CORRIGÉ, et c'est le plus important
`MergeSnapshots` fondait `entries` et `tombstones`, pas `folders`. Un dossier Trois trous, tous du même genre : une faute arrivait, et rien ne la nommait.
créé sur le téléphone — hors connexion, ou juste avant qu'un autre appareil
publie — disparaissait donc de la liste, **tandis que les entrées gardaient son
nom**. Elles se retrouvaient rangées dans un dossier que rien n'affiche :
introuvables sans qu'on le sache.
C'est exactement la panne contre laquelle `EnsureFolder` avait été écrit, et - **Les gestionnaires d'erreur lisaient l'exception après sa libération.**
elle est revenue par une autre porte. Le report est additif — on ne retire pas `RegisterLocal` et `LoginLocal` mettaient leur handler en file avec
un dossier du distant parce qu'il manque ici. `TThread.Queue`, puis y lisaient `E.Message` — or Delphi libère `E` en sortant
du `except`, bien avant que la file s'exécute. Résultat : un message vide, et
parfois une violation d'accès. **Le vrai motif n'a jamais été affiché de toute
l'histoire de ce chemin.**
- **Le dispatch du pont n'était gardé par rien** : une faute dans un handler
remontait à FMX, qui affichait « Access violation » avec une adresse pendant
que le journal montrait la commande reçue suivie de rien.
- **Le retour sur le thread principal** (`TThread.Queue`) n'était couvert ni par
le `except` du thread, ni par celui du pont : il s'exécute après que les deux
ont rendu la main.
### 2. Le témoin d'attente n'existait que sur la liste — CORRIGÉ **Ce que ça change pour les essais** : un échec porte désormais un nom. Si un
essai ci-dessous échoue sans laisser de ligne au journal, c'est un quatrième
trou du même genre, et il faut le traiter avant de chercher le défaut.
Le point orange était posé sur le cadenas, qui n'existe que dans la liste. Or ### 2. Deux secrets, longtemps confondus — CORRIGÉ
après un verrouillage on atterrit sur l'accueil — précisément là où l'on décide
de synchroniser. Il est maintenant aussi sur *Sync*.
### 3. Trois routines ne libèrent pas l'indicateur d'occupation dans un `finally` Le mot de passe de création scellait le conteneur local **et** prétendait
sceller l'instantané du dépôt. Un coffre créé ici publiait donc sous son propre
mot de passe, que le poste ne savait plus ouvrir.
`SaveAttachment`, `RegisterLocal` et `LoginLocal` le font dans le chemin normal - `FVaultPwd` ouvre `vault-local.json`, et rien d'autre.
**et** dans leur `except`, ce qui couvre tout ce qui descend d'`Exception`. Une - Le secret du dépôt vit dans les réglages, doit être identique sur tous les
erreur d'un autre genre laisserait le voile en place. appareils, et lui seul ouvre et rescelle l'instantané.
- **Ils ne se rencontrent qu'à un endroit**, la réconciliation : le conteneur
local s'ouvre sous le sien, puis la clé est jetée avant qu'on touche au dépôt.
**Non corrigé, et assumé** : le minuteur de 25 s le lève de toute façon, et Migration : rien à rechiffrer. Les coffres existants ont simplement les deux
c'est son rôle. Le signaler ici vaut mieux que d'ajouter trois `finally` qui valeurs égales, et cessent d'être forcés de le rester.
donneraient l'illusion d'une garantie que seul le minuteur apporte.
### 4. `ParseJSONValue(...) as TJSONObject` fuit sur une entrée qui n'est pas un objet ### 3. « Coffre ouvert » se lisait sur le nombre d'entrées — CORRIGÉ
Le transtypage lève avant l'affectation, donc la valeur analysée n'est jamais Un coffre **vide** en compte zéro, comme un coffre fermé. Le garde de `DoSync`
libérée. Dix-sept occurrences. **Non corrigé** : n'arrive que sur un conteneur restait donc vrai après l'ouverture, réarmait la reprise, et l'on tournait en
corrompu, où l'on a de plus gros ennuis, et le processus est de toute façon en rond. Le même idiome coupait `OnIdleTimer` : un coffre vide ne s'auto-verrouillait
train d'échouer bruyamment. À traiter le jour où l'on touche à ces fonctions. jamais. Les trois lisent `FPlain`, le clair en main.
### 5. Ce qui a été vérifié et va bien ### 4. Ce qui reste assumé, sans correction
- **Échappement HTML** : `esc()` couvre nom, sous-titre, initiale, URI d'icône, - **`SaveAttachment`, `RegisterLocal` et `LoginLocal`** libèrent l'indicateur
libellés, valeurs, noms de dossiers, options de liste. Une entrée nommée d'occupation dans leur `except` plutôt que dans un `finally`. Le minuteur de
`<script>` ne s'exécute pas. C'était le risque réel — le coffre contient du 25 s le lève de toute façon, et c'est son rôle.
texte arbitraire et la page construit son HTML par `innerHTML`. - **`ParseJSONValue(...) as TJSONObject`** fuit sur une entrée qui n'est pas un
- **La file de commandes du pont** libère `sending` dans un `finally` : une objet — le transtypage lève avant l'affectation. N'arrive que sur un conteneur
exception au retrait de l'iframe ne peut plus rendre l'interface muette. corrompu, où l'on a de plus gros ennuis.
- **Les états du `body`** (`typing`, `selecting`, `searching`, `on-list`, - **Le nom d'utilisateur ne sert à rien dans l'APK.** Il est écrit dans
`on-detail`, `is-busy`) ne portent le nom d'aucun élément — le piège qui l'instantané à la création, et c'est le **poste** qui s'en sert pour refuser de
éteignait la page entière ne se répète nulle part. fondre deux coffres différents. À la connexion il ne protège rien : le mot de
passe seul ouvre, et le vrai nom est lu dans le conteneur.
--- ---
@@ -64,15 +80,20 @@ train d'échouer bruyamment. À traiter le jour où l'on touche à ces fonctions
ce qu'il faut faire : un test dont on ne sait pas ce qu'il doit produire ne ce qu'il faut faire : un test dont on ne sait pas ce qu'il doit produire ne
prouve rien. prouve rien.
**Outil de remise à zéro** : *Settings → Debug → Delete the local vault* (deux
appuis). Il efface cache, conteneur local et drapeau d'attente, **et garde les
réglages du dépôt** — on retélécharge sans rien retaper. Absent en release : la
commande n'y est même pas compilée.
### A. Enregistrement hors connexion — le plus important ### A. Enregistrement hors connexion — le plus important
1. Coffre ouvert, **couper les données** (mode avion). 1. Coffre ouvert, **couper les données** (mode avion).
2. Modifier un mot de passe → message *« saved here, not published yet »*, et 2. Modifier un mot de passe → *« saved here, not published yet »*, et **point
**point orange** sur le cadenas. orange** sur le cadenas.
3. Verrouiller → le point doit être **sur le bouton Sync** de l'accueil. 3. Verrouiller → le point doit être **sur le bouton Sync** de l'accueil.
4. **Fermer complètement l'application** (glisser depuis les récentes), rouvrir, 4. **Fermer complètement l'application** (glisser depuis les récentes), rouvrir,
ouvrir le coffre → le point doit **être encore là**. C'est l'étape qui ouvrir le coffre → le point doit **être encore là**. C'est l'étape qui éprouve
éprouve la persistance du drapeau ; sans elle, le reste ne prouve rien. la persistance du drapeau ; sans elle, le reste ne prouve rien.
5. Rétablir le réseau, *Sync* → *« Offline changes published »*, point éteint. 5. Rétablir le réseau, *Sync* → *« Offline changes published »*, point éteint.
6. Sur le poste, synchroniser → la modification doit y être. 6. Sur le poste, synchroniser → la modification doit y être.
@@ -83,28 +104,37 @@ prouve rien.
3. Téléphone en ligne, *Sync*. 3. Téléphone en ligne, *Sync*.
4. **Les deux modifications doivent survivre.** Si une seule survit, c'est 4. **Les deux modifications doivent survivre.** Si une seule survit, c'est
l'arbitrage sur `updated_at` qu'il faut regarder. l'arbitrage sur `updated_at` qu'il faut regarder.
5. Variante avec un **dossier créé hors connexion** sur le téléphone : il doit 5. Variante avec un **dossier créé hors connexion** : il doit apparaître dans la
apparaître dans la barre de dossiers après la fusion, et sur le poste après barre de dossiers après la fusion, et sur le poste après sa synchronisation.
sa synchronisation. C'est le défaut n° 1 ci-dessus.
### C. Notes multilignes ### C. Les deux secrets — nouveau, et jamais éprouvé de bout en bout
1. **+** → *Note* → zone de texte pleine largeur, *Site* et *Username* disparus. 1. Effacer le coffre local (bouton Debug). Les trois champs du dépôt doivent
2. Taper plusieurs lignes — la touche Entrée doit **insérer un saut**, pas **rester remplis** — c'est la moitié de ce que ce bouton promet.
replier le clavier. 2. *Create vault* avec un mot de passe **différent** de celui du dépôt.
3. Enregistrer, rouvrir → les ruptures doivent être là **en lecture**. 3. Ouvrir les réglages : le champ *sync password* doit porter **celui du dépôt**,
4. Basculer *Login* → *Note* après avoir tapé : la saisie doit **survivre**. pas celui qu'on vient de choisir. C'est toute la séparation en une capture.
5. Contre-épreuve : dans un champ d'une ligne (*Name*, code de déverrouillage), 4. *Sync* → la réconciliation lit le dépôt et republie **sous le secret du
Entrée doit toujours **replier le clavier**. dépôt**. Contre-épreuve décisive : **le poste doit relire le dépôt sans se
plaindre.** S'il échoue, c'est que la clé du conteneur local a servi à
resceller l'instantané.
5. Après la réconciliation, `vault-local.json` doit avoir disparu — c'est ce
geste qui passe l'autorité au dépôt.
### D. Sélection et actions de liste ### D. Ouvrir demande quelque chose — nouveau
1. Appui long → barre contextuelle, compteur. 1. Effacer le coffre, puis *Download vault*.
2. Deuxième ligne → compteur à 2. 2. **Fermer complètement l'application**, rouvrir → le champ de code doit
3. **Dupliquer** → deux copies, suffixées *(copy)*. paraître et réclamer **le mot de passe de sync**. Le cache est scellé sous
4. **Supprimer** → confirmation en deux temps, puis les lignes disparaissent. lui, parce qu'il le contient déjà : l'ouvrir sans rien livrait le coffre *et*
5. Touche **retour** pendant une sélection → elle s'annule **sans verrouiller**. le secret du dépôt.
6. Sur le poste, synchroniser → les suppressions doivent y être répercutées. 3. Vérification par le fichier, si adb est à portée :
`run-as com.embarcadero.pmserverApk od -An -tu1 -N1 files/vault.bin` doit
rendre **4** (`VER_PIN`) et non 1.
4. Un **mauvais code** → *« Wrong unlock code »* **en rouge**, et *Open vault*
reste offert : le cache n'est pas déclaré illisible.
5. Sur un appareil **avec biométrie**, rien de tout cela : `VER_WRAP` garde déjà
la porte, et imposer une frappe serait un recul. C'est volontaire.
### E. Le dépôt a bougé — témoin bleu ### E. Le dépôt a bougé — témoin bleu
@@ -117,36 +147,39 @@ prouve rien.
met à jour toute seule, c'est que le sondage adopte au lieu de signaler. met à jour toute seule, c'est que le sondage adopte au lieu de signaler.
5. *Sync* → le point bleu s'éteint. 5. *Sync* → le point bleu s'éteint.
6. Les deux ensemble : provoquer A puis E sans synchroniser → **orange et bleu 6. Les deux ensemble : provoquer A puis E sans synchroniser → **orange et bleu
côte à côte**. Ils demandent deux gestes différents ; les confondre dirait côte à côte**. Ils demandent deux gestes différents.
« quelque chose ne va pas » sans dire quoi.
### F. Réglages ### F. Réglages — largement réécrits, à reprendre en entier
1. Roue dentée dans la liste, ou *Settings* sur l'accueil → l'écran s'ouvre. 1. **Coffre fermé**, ouvrir les réglages depuis l'accueil → la section *Unlock
2. Les trois champs du dépôt doivent porter **les valeurs en place**, pas du code* doit être **absente**. Poser comme retirer exigent le clair en main :
vide : ils ne sont qu'une vue des champs que Delphi connaît déjà. coffre fermé, les deux ne pourraient que refuser.
3. Modifier l'URL, revenir par la flèche, *Sync* → la nouvelle valeur sert. 2. Ouvrir le coffre, roue dentée → la section paraît.
C'est la contre-épreuve du report à la fermeture : un réglage modifié puis 3. **Poser un code** : taper, *Set this code* → *« Unlock code set »*
refermé **sans quitter le champ** doit compter. **immédiatement, sans quitter l'écran**. Plus de « ouvre le coffre pour
4. Touche **retour** dans les réglages → elle referme **sans** verrouiller ni l'appliquer » : le code s'applique sur-le-champ.
revenir à la liste. 4. Le message doit se voir **au-dessus du clavier resté ouvert**. C'est le
5. **Poser un code** depuis les réglages : taper, *Set this code*, puis ouvrir défaut le plus discret de la séance : le toast vivait sous le clavier.
le coffre → *« Unlock code changed »*. Le sceau se pose toujours à 5. **Un seul appui** doit suffire sur *Set this code*. S'il en faut deux, le
l'ouverture, et nulle part ailleurs. premier est mangé par le clavier et la correction du focus a régressé.
6. **Retirer le code** : *Remove the unlock code*, deux appuis → *« Unlock code 6. **Retirer le code** : *Remove the unlock code* sans rien taper → refus
removed »*. Fermer l'application, rouvrir → **le code n'est plus demandé**. *« Type the current unlock code to remove it »*. Puis taper le code courant,
7. Contre-épreuve du retrait : verrouiller, puis essayer de retirer le code deux appuis → *« Unlock code removed »*. Fermer l'application, rouvrir → **le
sans avoir ouvert le coffre → refus *« Open the vault first »*. C'est ce qui code n'est plus demandé**.
empêche quelqu'un tenant le téléphone déverrouillé de lever la protection 7. Contre-épreuve : *Set this code* avec un champ **vide** ne doit **pas** retirer
sans la connaître. le code — c'était une porte dérobée vers le retrait sans le connaître.
8. Le champ *sync password* doit porter le mot de passe du coffre **détenu**, pas
une valeur de premier lancement.
9. Touche **retour** dans les réglages → referme **sans** verrouiller ni changer
de vue.
### G. Icônes ### G. Icônes
1. Ouvrir une entrée dont le *Site* est une adresse — `git.ai-agents4you.com`. 1. Ouvrir une entrée dont le *Site* est une adresse — `git.ai-agents4you.com`.
2. Trois points → **Fetch icon** → l'icône doit apparaître dans la liste. 2. Trois points → **Fetch icon** → l'icône doit apparaître dans la liste.
3. Une entrée dont le site n'est **pas** une adresse (*Gitea*, ou vide) → 3. Une entrée dont le site n'est **pas** une adresse (*Gitea*, ou vide) → refus
refus *« no web address to look up »*. Sans point dans le nom, il n'y a *« no web address to look up »*. Sans point dans le nom, il n'y a rien à
rien à demander au relais. demander au relais.
4. Un domaine à trois niveaux (`chat.qwen.ai`) doit retomber sur le second 4. Un domaine à trois niveaux (`chat.qwen.ai`) doit retomber sur le second
niveau : c'est le deuxième essai. niveau : c'est le deuxième essai.
5. **Remove icon** → l'entrée reprend son initiale. 5. **Remove icon** → l'entrée reprend son initiale.
@@ -156,50 +189,85 @@ prouve rien.
### H. Anciens mots de passe ### H. Anciens mots de passe
**Le poste doit synchroniser d'abord** : il ne les envoie que depuis ce soir, **Le poste doit synchroniser d'abord** : il ne les envoie que depuis peu, et un
et un instantané publié avant ne porte aucun historique. instantané publié avant ne porte aucun historique.
1. Sur le poste, changer le mot de passe d'une entrée, puis synchroniser. 1. Sur le poste, changer le mot de passe d'une entrée, puis synchroniser.
2. Téléphone : *Sync*, ouvrir l'entrée, trois points → **Previous passwords**. 2. Téléphone : *Sync*, ouvrir l'entrée, trois points → **Previous passwords**.
L'ancienne version doit être là, avec sa date. L'ancienne version doit être là, avec sa date.
3. Copier l'une d'elles → le presse-papiers doit la porter, et le message dire 3. Copier l'une d'elles → *« Previous password copied »*.
*« Previous password copied »*.
4. Touche **retour** → le voile se referme sans toucher au coffre. 4. Touche **retour** → le voile se referme sans toucher au coffre.
5. Une entrée jamais modifiée → *« No previous password recorded »*, et non un 5. Une entrée jamais modifiée → *« No previous password recorded »*, et non un
écran vide. Un instantané d'avant ce soir donne le même message : c'est écran vide.
volontaire, et le libellé le dit plutôt que de laisser chercher une panne. 6. Contre-épreuve : verrouiller, rouvrir, **Previous passwords** → la liste doit
6. Contre-épreuve : verrouiller, rouvrir, **Previous passwords** → la liste être reconstruite, jamais celle d'avant restée à l'écran.
doit être reconstruite, jamais celle d'avant restée à l'écran.
### I. Pièces jointes ### I. Clavier, mise en page, retour d'application — nouveau
1. Taper dans un champ, appuyer sur un bouton **hors de sa rangée** (*Open
vault*, *Sign in*, *Set this code*) → **un seul appui**, et le clavier se
replie ensuite.
2. Taper dans un champ, appuyer sur un bouton **de sa rangée** (l'œil d'un champ
secret) → le clavier **reste ouvert** et le curseur en place. Les deux moitiés
comptent : la première sans la seconde laisse le clavier dressé.
3. **Réglages ouverts coffre FERMÉ**, verrouiller le téléphone, déverrouiller →
la page doit revenir. C'était un aplat bleu sans rien dessus : le masquage ne
demandait rien, la restitution exigeait un coffre ouvert.
4. Même chose **coffre ouvert** : passé le délai de grâce, le verrouillage doit
avoir lieu et ramener sur l'accueil — pas sur la liste.
5. Accueil sans coffre : l'ordre doit être **Download vault → Settings →
username / password → Create vault**, espacements réguliers, et *Download
vault* doit porter la couleur d'accent quand un dépôt est configuré.
6. Les boutons du bandeau (roue, +, cadenas) et ceux de la barre de sélection
sont **ronds**.
### J. Sélection et actions de liste
1. Appui long → barre contextuelle, compteur.
2. Deuxième ligne → compteur à 2.
3. **Dupliquer** → deux copies, suffixées *(copy)*.
4. **Supprimer** → confirmation en deux temps, puis les lignes disparaissent.
5. Touche **retour** pendant une sélection → elle s'annule **sans verrouiller**.
6. Sur le poste, synchroniser → les suppressions doivent y être répercutées.
### K. Notes multilignes
1. **+** → *Note* → zone de texte pleine largeur, *Site* et *Username* disparus.
2. Taper plusieurs lignes — Entrée doit **insérer un saut**, pas replier le
clavier.
3. Enregistrer, rouvrir → les ruptures doivent être là **en lecture**.
4. Basculer *Login* → *Note* après avoir tapé : la saisie doit **survivre**.
5. Contre-épreuve : dans un champ d'une ligne, Entrée doit toujours **replier le
clavier**.
### L. Pièces jointes
1. Ligne *Attachment* → flèche de téléchargement → le fichier apparaît dans 1. Ligne *Attachment* → flèche de téléchargement → le fichier apparaît dans
*Téléchargements* sous son nom, accents et arabe compris. *Téléchargements* sous son nom, accents et arabe compris.
### J. Code de déverrouillage ### M. Empaquetage
1. Poser un code, ouvrir le coffre, fermer, rouvrir → il est demandé.
2. Code faux → *« Wrong unlock code »*, et **Open vault reste offert** (le
coffre n'est pas déclaré illisible).
3. Changer le code : ancien dans le premier champ, nouveau dans le second,
*Open vault* → *« Unlock code changed »*.
4. Contre-épreuve : taper n'importe quoi dans le champ du code puis *Sync* →
le code **ne doit pas changer**.
### K. Empaquetage
1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher 1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher
`Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android `Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android Debug`,
Debug`, la release ne vaut rien. la release ne vaut rien.
2. Elle ne s'installe pas d'elle-même, et c'est voulu. 2. Elle ne s'installe pas d'elle-même, et c'est voulu.
3. Vérifier que la section **Debug** des réglages est **absente** de cette
version. Elle ne peut pas y être — la commande n'est pas compilée — mais
c'est le genre de garantie qu'on regarde une fois plutôt que de la croire.
--- ---
## Ce qui n'est toujours pas éprouvé ## Ce qui n'est toujours pas éprouvé
- **Le chemin `-Release`** du script n'a jamais tourné. - **Le chemin `-Release`** du script n'a jamais produit d'APK signé. Il compile,
- **§1 point 4** — l'invalidation de la clé par enrôlement d'une empreinte, c'est tout ce qu'on sait.
désamorcée par le code de déverrouillage tant qu'un code est posé. - **L'invalidation de la clé par le système** (§ D.5 pour la biométrie) : le
- **`MergeSelfCheck`** tourne au démarrage en débogage et écrit message *« Device protection changed »* existe et son drapeau est posé là où
`merge selfcheck: the four rules hold` dans le journal. Personne ne l'a l'invalidation se constate, mais personne ne l'a vu s'afficher. Il faudrait
encore lu. retirer le verrouillage d'écran d'un appareil qui en a un.
- **`MergeSelfCheck`** tourne au démarrage en débogage et écrit `merge selfcheck:
the four rules hold` dans le journal. Il passe ; personne ne l'a lu en
cherchant à le mettre en défaut.
- **Le mode `login`** (conteneur local sans cache) ne s'atteint qu'en supprimant
le cache à la main. C'est là qu'a été trouvée la lecture après libération : il
n'avait jamais tourné.