test(e2e): le parcours d'un primo-arrivant, et un harnais qui échoue au lieu de se pendre

Aucun parcours n'avait jamais marché le chemin d'un nouvel arrivant : tous
pré-injectaient l'identifiant dans l'URL, ce qui fait résoudre l'identité sans
jamais afficher la barrière. La suite était verte par-dessus un lien de
téléchargement pointant sur un fichier que personne ne servait — et le serveur
de test répondait la page HTML de l'application pour tout chemin inconnu, donc
un fichier manquant ne POUVAIT pas échouer.

Le nouveau parcours part d'un profil vide : barrière, téléchargement réel,
import dans l'application portefeuille, saisie de l'identifiant, remise au
broker, retour dans l'iframe. Il vérifie neuf points, dont celui qui compte —
l'identifiant a survécu et l'identité rapportée est celle qui a été saisie.

Le harnais, lui, se pendait au lieu d'échouer. Cause observée : le tuyau
devtools de Chromium lâche et Playwright n'émet ni close ni disconnected, si
bien que la suite bloquait dans son propre nettoyage sans imprimer ni résumé ni
l'échec déjà en route. Toutes les attentes sont désormais bornées et nomment ce
qu'elles attendaient ; vérifié en cassant délibérément une attente, et observé
en conditions réelles — trois minutes et « gave up waiting for: alice to sign
in » là où j'ai tué trois exécutions d'une heure ce matin.

Deux exécutions simultanées ne se détruisent plus : verrou atomique sur le
profil, et récupération d'un navigateur laissé par une exécution tuée. Le
marqueur devient .user-consumed — il n'a jamais attesté d'une disponibilité,
seulement qu'un lot avait déjà pris l'utilisateur de ce profil. Au passage, la
destruction du profil dépendait du marqueur, écrit en FIN de lot : une
exécution tuée avant laissait un profil que la suivante réutilisait, et
héritait de sa casse. Elle dépend maintenant du profil.

La suite applicative reste non mesurée sur cette machine : un conteneur en
boucle de redémarrage recycle son interface réseau, et sept exécutions sur dix
échouent sur le transport. Trois sont passées 21/21.
This commit is contained in:
Sylvain Duchesne
2026-08-11 19:34:09 +02:00
parent 7c2e8d8f1f
commit c5b4703687
9 changed files with 1000 additions and 103 deletions
@@ -64,7 +64,7 @@ export function subscribeDocs(nuris: NuriLike[], onChange: (r: DocChange, t: Doc
// ── low-level document / SPARQL primitives ───────────────────────────────
export const docs: {
docCreate(sessionId: string, crdt?: string, cls?: string, dest?: string, store?: unknown): Promise<Nuri>;
docCreate(sessionId: string, crdt: string, cls: string, dest: string, store?: unknown): Promise<Nuri>;
sparqlQuery(sessionId: string, query: string, base?: string, anchor?: NuriLike, label?: string): Promise<unknown>;
sparqlUpdate(sessionId: string, query: string, anchor?: NuriLike, label?: string): Promise<void>;
};
@@ -14,13 +14,15 @@ They must stay apart, and the reason is not tidiness.
## Why they cannot be one call
`init()` hands the page over synchronously when the page is top-level, so nothing placed after it runs. The `barrier` must therefore appear **before** `init()`.
`init()` starts the broker redirect as its first statement when the page is top-level, so anything the barrier needs to show must already be in place. The `barrier` must therefore appear **before** `init()`.
But an identity cannot connect before `init()` either: connecting reaches the application's session thunk, which only `init()`'s own callback resolves. Await the whole of sign-in before `init()` and it deadlocks; call it after and the `barrier` never appears.
The way out is that only *half* of it has that dependency. Settling is awaited by this package's `init()` wrapper before it delegates; connecting stays in the published call, awaited where a session exists.
**The ordering invariant is carried by the composition, not by a documented call order** — which is why an application must call this package's `init`, not the one it injected. A note telling callers to order two calls correctly is not a mechanism.
**The invariant is carried by the composition from `init()` onwards** — which is why an application must call this package's `init`, not the one it injected: calling it settles the identity whether or not the published call has run.
It does **not** cover calling the published sign-in strictly first and awaiting it: the connection work it adds waits for a session only `init()` resolves, so it deadlocks in silence. Half an invariant carried by a mechanism, half still owed to the caller — and the half still owed fails silently, which is the worst kind.
## The failure this cost