Files
ng-eventually/docs/briefs/2026-08-10-consumer-harness-identity-switching.md
T
Sylvain Duchesne b7dc8ca2c3 fix: la suite n'était pas hermétique, et deux tests ne pouvaient pas échouer
Second tour adverse sur le lot D. Trois trouvailles, et une erreur de diagnostic de ma
part qui vaut d'être consignée.

**La suite verte dépendait de l'ordre des fichiers.** `bun test isolation-active
public-store` donnait 5 échecs quand chaque fichier seul était vert — donc un checkout de
CI avec un autre ordre d'inodes livrait rouge. Deux causes distinctes :

- le travail de connexion, lancé sans être attendu par `setCurrentUser`, débordait d'un
  fichier sur le suivant et armait l'émulation. `connectedUser` abandonne désormais dès
  que l'identité pour laquelle il a démarré n'est plus connectée — ce qui est de toute
  façon la bonne sémantique : en amont une session appartient à un utilisateur, et
  changer d'utilisateur est une autre session ;
- et surtout **mon propre test de store public exposait le cap d'un document que
  personne ne détient** — un état que la bibliothèque ne produit jamais. Il ne passait
  que tant que l'émulation était désarmée. Alice crée sa note avant de l'exposer,
  maintenant. Balayage des 21 paires de fichiers : plus aucune ne pollue.

**Le contrôle symétrique ajouté hier ne pouvait pas échouer.** « La liste d'Alice ne
contient pas la note de Bob » lisait un rendu ANTÉRIEUR à l'écriture de Bob : l'attente
de `showScope` était satisfaite au premier sondage par le marqueur déjà à l'écran, sans
synchroniser quoi que ce soit. Alice écrit désormais une note APRÈS celle de Bob —
`writeNote` attend son apparition, donc ce qui suit est un rendu qui post-date. Et le
`.catch` qui avalait le délai d'attente est retiré : une liste qui ne se stabilise jamais
est un échec à voir, pas une dégradation à absorber.

**Le test anti-fork prouvait « pas le premier », pas « le canonique ».** Son minimum
lexicographique était aussi le DERNIER élément, si bien qu'un choix positionnel — la
faute exacte que ce test existe pour attraper — restait vert. Le minimum est déplacé au
milieu ; vérifié par mutation, « prendre le dernier » le fait rougir.

**Mon erreur de diagnostic.** J'ai cru trouver, sous la trouvaille d'ordre, une fuite
entre utilisateurs — les caps d'Alice classés chez Bob — et je l'ai « reproduite ». Le
repro était faux : son faux `ng` ignorait le sujet dans la requête d'inbox, donc l'inbox
de Bob résolvait vers celle d'Alice. Une fois le faux corrigé, la fuite ne se reproduit
plus, ni avec ni sans correctif. Le danger reste réel en lecture du code — trois chemins
classent des caps plusieurs `await` après la garde qui les autorisait — donc
`caps.holderKey`/`learnFor` le ferment par construction, mais les commentaires disent
maintenant ce que c'est : un risque fermé, pas un défaut observé.

189 tests unitaires, e2e 40/40 et applicatif 12/12.
2026-08-10 10:02:45 +02:00

3.8 KiB

A consumer's test harness has no way to play several identities

Raised by the first consumer (Festipod) on 2026-08-10, while migrating onto @ng-eventually/sdk against contract_sdk-surface @ 30f6263. Three findings, one substantive and two defects in the contract's own text.

1. The substantive gap — a consumer harness cannot switch identity

contract_sdk-surface § Guarantees states the whole of signing in: ensureIdentity(), which takes no identifier, deliberately. The API contract adds, about setCurrentUser's removal, that "the e2e harness plays several identities on one page and reaches it by its internal path, which is what a harness is allowed to do and an application is not."

That sentence holds for this library's own harness. It does not hold for a consumer's, and the package makes sure of it: packages/sdk/package.json maps exactly one entry, "." : "./src/index.ts". A deep import is refused by the resolver, verified from the consumer's tree:

ROOT OK:  configure, docChangeType, docs, ensureIdentity, inbox, init, initNg,
          ng, readUnion, storeRegistry, subscribeDoc, subscribeDocs, useShape, watchShape
DEEP FAIL: Cannot find module '@ng-eventually/sdk/src/shared-wallet/access-gate'

So a consumer's multi-actor suite has no path at all — published or internal — to act as a second identity. The consequence is not cosmetic: a test that cannot obtain a second actor is forced to hand the first actor's values across the identity boundary through a shared variable, which is precisely the shape that hid a real bug in this library once already (a third party never reached the owner's inbox, and the green test proved nothing because the address crossed the boundary by JS scope). Losing the ability to write that test correctly costs more than the surface it saves.

What the consumer needs is narrow: act as identity X for the duration of a block, then restore. It is a harness capability, not an application one — the request is not to re-publish setCurrentUser on the application surface. A separate, explicitly-named test entry (@ng-eventually/sdk/testing, say) would keep the application surface exactly as it is while making the capability reachable; it would also carry its own deletion signal, since a consumer harness that plays several identities on one page is itself pure shared-wallet scaffolding.

Not proposing the shape — this is the library's call. Stating the need, and that it currently has no answer.

2. watchShape's published signature contradicts its own types

## Surface publishes:

export function watchShape(query: ShapeQuery): ShapeObservable;

ShapeQuery is the result type ({ data, isPending, isSuccess, isError, error }, per docs/api-contract.md § 5), so as written the call takes its own return value. The signature the consumer has always called, and the one § 5 documents, is watchShape<T>(shapeType, scope) — two positional arguments. One of the two documents is wrong; the contract is the one consumers read.

3. There is no synced read for the per-document form

The inbox surface publishes readSynced(targetInbox) and readForDocument(doc), but not their intersection. The consumer's materialization path depends on the synced guarantee specifically (read and readSynced differ by contract), and it addresses by document. Today it must therefore resolve an address itself to get the synced form — which is the exact gesture § Guarantees says an application never performs ("an application never handles a key or an inbox address").

Either readForDocument carries the synced guarantee, or a readSyncedForDocument(doc) completes the pair. As it stands the document-addressed path is strictly weaker than the address-addressed one, and the contract does not say that is intentional.