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
Relecture du 2026-08-22, avant d'ajouter la vérification de fond, les réglages
et les icônes. Le but n'était pas de tout relire mais de chercher là où cette
séance a montré que les défauts se logent : **les frontières entre fichiers,
les chemins d'erreur, et ce qui traverse les threads.**
Réécrit le **2026-08-28**, après une séance qui a remué les deux secrets du
coffre, l'écran des réglages, le code de déverrouillage et le clavier. La
version précédente décrivait une application qui n'existe plus.
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
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.
Trois trous, tous du même genre : une faute arrivait, et rien ne la nommait.
C'est exactement la panne contre laquelle `EnsureFolder` avait été écrit, et
elle est revenue par une autre porte. Le report est additif — on ne retire pas
un dossier du distant parce qu'il manque ici.
- **Les gestionnaires d'erreur lisaient l'exception après sa libération.**
`RegisterLocal` et `LoginLocal` mettaient leur handler en file avec
`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
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*.
### 2. Deux secrets, longtemps confondus — CORRIGÉ
### 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
**et** dans leur `except`, ce qui couvre tout ce qui descend d'`Exception`. Une
erreur d'un autre genre laisserait le voile en place.
- `FVaultPwd` ouvre `vault-local.json`, et rien d'autre.
- Le secret du dépôt vit dans les réglages, doit être identique sur tous les
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
c'est son rôle. Le signaler ici vaut mieux que d'ajouter trois `finally` qui
donneraient l'illusion d'une garantie que seul le minuteur apporte.
Migration : rien à rechiffrer. Les coffres existants ont simplement les deux
valeurs égales, et cessent d'être forcés de le rester.
### 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
libérée. Dix-sept occurrences. **Non corrigé** : n'arrive que sur un conteneur
corrompu, où l'on a de plus gros ennuis, et le processus est de toute façon en
train d'échouer bruyamment. À traiter le jour où l'on touche à ces fonctions.
Un coffre **vide** en compte zéro, comme un coffre fermé. Le garde de `DoSync`
restait donc vrai après l'ouverture, réarmait la reprise, et l'on tournait en
rond. Le même idiome coupait `OnIdleTimer` : un coffre vide ne s'auto-verrouillait
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,
libellés, valeurs, noms de dossiers, options de liste. Une entrée nommée
`<script>` ne s'exécute pas. C'était le risque réel — le coffre contient du
texte arbitraire et la page construit son HTML par `innerHTML`.
- **La file de commandes du pont** libère `sending` dans un `finally` : une
exception au retrait de l'iframe ne peut plus rendre l'interface muette.
- **Les états du `body`** (`typing`, `selecting`, `searching`, `on-list`,
`on-detail`, `is-busy`) ne portent le nom d'aucun élément — le piège qui
éteignait la page entière ne se répète nulle part.
- **`SaveAttachment`, `RegisterLocal` et `LoginLocal`** libèrent l'indicateur
d'occupation dans leur `except` plutôt que dans un `finally`. Le minuteur de
25 s le lève de toute façon, et c'est son rôle.
- **`ParseJSONValue(...) as TJSONObject`** fuit sur une entrée qui n'est pas un
objet — le transtypage lève avant l'affectation. N'arrive que sur un conteneur
corrompu, où l'on a de plus gros ennuis.
- **Le nom d'utilisateur ne sert à rien dans l'APK.** Il est écrit dans
l'instantané à la création, et c'est le **poste** qui s'en sert pour refuser de
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
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
1. Coffre ouvert, **couper les données** (mode avion).
2. Modifier un mot de passe → message *« saved here, not published yet »*, et
**point orange** sur le cadenas.
2. Modifier un mot de passe → *« saved here, not published yet »*, et **point
orange** sur le cadenas.
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,
ouvrir le coffre → le point doit **être encore là**. C'est l'étape qui
éprouve la persistance du drapeau ; sans elle, le reste ne prouve rien.
ouvrir le coffre → le point doit **être encore là**. C'est l'étape qui éprouve
la persistance du drapeau ; sans elle, le reste ne prouve rien.
5. Rétablir le réseau, *Sync* → *« Offline changes published »*, point éteint.
6. Sur le poste, synchroniser → la modification doit y être.
@@ -83,28 +104,37 @@ prouve rien.
3. Téléphone en ligne, *Sync*.
4. **Les deux modifications doivent survivre.** Si une seule survit, c'est
l'arbitrage sur `updated_at` qu'il faut regarder.
5. Variante avec un **dossier créé hors connexion** sur le téléphone : il doit
apparaître dans la barre de dossiers après la fusion, et sur le poste après
sa synchronisation. C'est le défaut n° 1 ci-dessus.
5. Variante avec un **dossier créé hors connexion** : il doit apparaître dans la
barre de dossiers après la fusion, et sur le poste après sa synchronisation.
### 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.
2. Taper plusieurs lignes — la touche 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 (*Name*, code de déverrouillage),
Entrée doit toujours **replier le clavier**.
1. Effacer le coffre local (bouton Debug). Les trois champs du dépôt doivent
**rester remplis** — c'est la moitié de ce que ce bouton promet.
2. *Create vault* avec un mot de passe **différent** de celui du dépôt.
3. Ouvrir les réglages : le champ *sync password* doit porter **celui du dépôt**,
pas celui qu'on vient de choisir. C'est toute la séparation en une capture.
4. *Sync* → la réconciliation lit le dépôt et republie **sous le secret du
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.
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.
1. Effacer le coffre, puis *Download vault*.
2. **Fermer complètement l'application**, rouvrir → le champ de code doit
paraître et réclamer **le mot de passe de sync**. Le cache est scellé sous
lui, parce qu'il le contient déjà : l'ouvrir sans rien livrait le coffre *et*
le secret du dépôt.
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
@@ -117,36 +147,39 @@ prouve rien.
met à jour toute seule, c'est que le sondage adopte au lieu de signaler.
5. *Sync* → le point bleu s'éteint.
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
« quelque chose ne va pas » sans dire quoi.
côte à côte**. Ils demandent deux gestes différents.
### 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.
2. Les trois champs du dépôt doivent porter **les valeurs en place**, pas du
vide : ils ne sont qu'une vue des champs que Delphi connaît déjà.
3. Modifier l'URL, revenir par la flèche, *Sync* → la nouvelle valeur sert.
C'est la contre-épreuve du report à la fermeture : un réglage modifié puis
refermé **sans quitter le champ** doit compter.
4. Touche **retour** dans les réglages → elle referme **sans** verrouiller ni
revenir à la liste.
5. **Poser un code** depuis les réglages : taper, *Set this code*, puis ouvrir
le coffre → *« Unlock code changed »*. Le sceau se pose toujours à
l'ouverture, et nulle part ailleurs.
6. **Retirer le code** : *Remove the unlock code*, deux appuis → *« Unlock code
removed »*. Fermer l'application, rouvrir → **le code n'est plus demandé**.
7. Contre-épreuve du retrait : verrouiller, puis essayer de retirer le code
sans avoir ouvert le coffre → refus *« Open the vault first »*. C'est ce qui
empêche quelqu'un tenant le téléphone déverrouillé de lever la protection
sans la connaître.
1. **Coffre fermé**, ouvrir les réglages depuis l'accueil → la section *Unlock
code* doit être **absente**. Poser comme retirer exigent le clair en main :
coffre fermé, les deux ne pourraient que refuser.
2. Ouvrir le coffre, roue dentée → la section paraît.
3. **Poser un code** : taper, *Set this code* → *« Unlock code set »*
**immédiatement, sans quitter l'écran**. Plus de « ouvre le coffre pour
l'appliquer » : le code s'applique sur-le-champ.
4. Le message doit se voir **au-dessus du clavier resté ouvert**. C'est le
défaut le plus discret de la séance : le toast vivait sous le clavier.
5. **Un seul appui** doit suffire sur *Set this code*. S'il en faut deux, le
premier est mangé par le clavier et la correction du focus a régressé.
6. **Retirer le code** : *Remove the unlock code* sans rien taper → refus
*« Type the current unlock code to remove it »*. Puis taper le code courant,
deux appuis → *« Unlock code removed »*. Fermer l'application, rouvrir → **le
code n'est plus demandé**.
7. Contre-épreuve : *Set this code* avec un champ **vide** ne doit **pas** retirer
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
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.
3. Une entrée dont le site n'est **pas** une adresse (*Gitea*, ou vide) →
refus *« no web address to look up »*. Sans point dans le nom, il n'y a
rien à demander au relais.
3. Une entrée dont le site n'est **pas** une adresse (*Gitea*, ou vide) → refus
*« no web address to look up »*. Sans point dans le nom, il n'y a rien à
demander au relais.
4. Un domaine à trois niveaux (`chat.qwen.ai`) doit retomber sur le second
niveau : c'est le deuxième essai.
5. **Remove icon** → l'entrée reprend son initiale.
@@ -156,50 +189,85 @@ prouve rien.
### H. Anciens mots de passe
**Le poste doit synchroniser d'abord** : il ne les envoie que depuis ce soir,
et un instantané publié avant ne porte aucun historique.
**Le poste doit synchroniser d'abord** : il ne les envoie que depuis peu, et un
instantané publié avant ne porte aucun historique.
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**.
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
*« Previous password copied »*.
3. Copier l'une d'elles → *« Previous password copied »*.
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
écran vide. Un instantané d'avant ce soir donne le même message : c'est
volontaire, et le libellé le dit plutôt que de laisser chercher une panne.
6. Contre-épreuve : verrouiller, rouvrir, **Previous passwords** → la liste
doit être reconstruite, jamais celle d'avant restée à l'écran.
écran vide.
6. Contre-épreuve : verrouiller, rouvrir, **Previous passwords** → la liste 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
*Téléchargements* sous son nom, accents et arabe compris.
### J. Code de déverrouillage
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
### M. Empaquetage
1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher
`Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android
Debug`, la release ne vaut rien.
`Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android Debug`,
la release ne vaut rien.
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é
- **Le chemin `-Release`** du script n'a jamais tourné.
- **§1 point 4** — l'invalidation de la clé par enrôlement d'une empreinte,
désamorcée par le code de déverrouillage tant qu'un code est posé.
- **`MergeSelfCheck`** tourne au démarrage en débogage et écrit
`merge selfcheck: the four rules hold` dans le journal. Personne ne l'a
encore lu.
- **Le chemin `-Release`** du script n'a jamais produit d'APK signé. Il compile,
c'est tout ce qu'on sait.
- **L'invalidation de la clé par le système** (§ D.5 pour la biométrie) : le
message *« Device protection changed »* existe et son drapeau est posé là où
l'invalidation se constate, mais personne ne l'a vu s'afficher. Il faudrait
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é.