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.
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.