# What a consumer's tests need from the contract, and cannot find in it Raised by the first consumer (Festipod) on 2026-08-10, while migrating onto `@ng-eventually/sdk` against `contract_sdk-surface` @ `30f6263`. Three findings. ## 1. The contract does not say how an identity comes to be established The first version of this brief asked for a test entry exposing identity switching, on the strength of the API contract's remark 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"* — true of this library's own harness, and unreachable for a consumer's, since `packages/sdk/package.json` maps exactly one entry and the resolver refuses a deep import (verified: `Cannot find module '@ng-eventually/sdk/src/shared-wallet/access-gate'`). **That request is withdrawn**, and the reason is worth recording because it is the library's own argument turned around. The consumer decided that its tests take no shortcut through the SDK and validate the application's behaviour rather than the SDK's. Under that rule, "two identities on one page" is not a capability to restore: it is not something a user does, it exists only because one wallet hosts several identities, and a test that used it would be testing the emulation. Multi-user behaviour gets tested the way it is lived — several browser contexts, each signing in as itself. So the surface is right as it stands, and the library should not add a testing entry on this consumer's account. **A larger claim was drafted here and is retracted before it could mislead.** It said the contract offers no way to determine which identity comes up, and that a consumer therefore has no reproducible test isolation. That is wrong: `EventuallyConfig` publishes both `sharedWallet` and `currentUser`, so bootstrap is exactly where a caller says which wallet to open and, when it must, which identity to come up as. The consumer had simply not wired those two fields yet — its own migration was incomplete, and the library's error message said so precisely (*"no shared wallet configured. Pass `sharedWallet` to `configure()`"*). A good error message did the work the brief was trying to do. What remains is small and is prose, not surface. `## Guarantees` describes `ensureIdentity()` as *"the whole of signing in… it resolves who you are"* without ever connecting that sentence to the two `EventuallyConfig` fields that determine the answer. The `## Surface` block carries them as bare lines in a config interface, where nothing marks them as the inputs to the one call a consumer must get right before anything renders. Naming that link — *these fields are what `ensureIdentity()` resolves from* — would have saved a wrong diagnosis here, and it is one sentence. ## 2. `watchShape`'s published signature contradicts its own types `## Surface` publishes: ```ts 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(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. ## 4. Identity was taken out of the application's hands, but three published calls still demand one This is the finding that actually cost the migration, and it is one incoherence seen from two sides. `ensureIdentity()` returns `void`, and nothing else answers *"who am I"* — `getCurrentUser` was removed on the sound argument that an application knows who it signed in. Under the previous surface that was true: the application named the identity, so it held the value. It no longer names it, and the gate that resolves it hands nothing back. So the premise the removal rested on has quietly stopped holding. Meanwhile `storeRegistry.createEntityDoc(id, scope)`, `listMyEntityDocs(id, scope)` and `resolveWriteGraph(id, scope)` all take a mandatory `id: string`, and **the contract never says what it is**. Two readings were open. **The library has now answered, by refusing — and the answer is measured, not argued.** The consumer bet on the harmless reading (`id` = a collection key inside the already-connected identity, which `listMy…` suggests) and routed all its call sites through one constant, because the other reading offered it no legal move at all. Running the broker-backed suite settles it in one line. Each scenario opens as its own identity — verified, the access log shows `test-…-2`, `-3`, `-4`, `-5`, `-6`, `-7` — and every one of them fails on **the same document**: ``` [test-…-2][polyfill] createEntityDoc cap append failed: [ng-eventually] createEntityDoc:addRepo: refused — the connected user does not hold this document's cap. Naming a document does not grant access to it. "did:ng:o:TFYUlJQnOkYUlC4T9XtGOBoz70opW_IO21VJ_ouF_Y0A:v:…" ``` The first identity creates that document and owns it; every later identity names the same one and is refused. So **`id` is per-identity**, and a constant collapses every user onto one owner's document. Two things follow, and the second is the blocker. **The good news, worth saying plainly: the guarantee held.** The failure mode feared here was a silent merge — every user's documents in one collection, no error, isolation quietly gone. That is not what happened. *"Writing is ownership"* refused loudly and named the reason, and a consumer's wrong guess became a red test instead of a data leak. This is the surface doing exactly what it promises. **The blocker: the three calls require the connected identity, and no published call returns it.** `ensureIdentity()` resolves it and returns `void`; `getCurrentUser` was removed on the argument that an application knows who it signed in — which was true while the application named the identity and stopped being true the moment it no longer does. So the consumer must pass a value it has no way to obtain. It currently works only where a deployment plants one (its test environment, through `configure({ currentUser })`); a real deployment plants nothing, and there the application is stuck. The pair has to close, either way round: the three calls stop taking an `id` (the session IS the identity — what the API contract already predicts for the target), or the surface answers *"who am I"* again. Right now it does neither, and this is no longer a documentation nicety — it is the one thing standing between a consumer and a working multi-user deployment. ## 6. The barrier is the library's, and nothing can test that a first-time device gets in The consumer has now deleted its own access screen and relies on the barrier `ensureIdentity()` shows — which is right, and is what the library asked for when it absorbed ~300 lines of gate from this same application. Its one end-to-end check of a first-time device — a real browser with an empty profile, offered the wallet file and its password, importing it, and coming back signed in — drove that deleted screen's own DOM, so it went with the screen. The consumer still needs that coverage: a first-time device getting in is the single most consequential path a real user takes, and it is currently covered by nothing. **The consumer is not asking for testids, and is rebuilding the check on its own side.** An end-to-end test should interact the way a person does — visible text and roles — rather than reach for identifiers planted inside someone else's component, so driving your barrier that way needs nothing published and is the more honest test besides. **One question, because it decides whether that is possible at all**: is the barrier reachable by ordinary browser automation? A screen rendered inside a **shadow root**, or in its own iframe, is invisible to a locator that searches the light DOM, and no amount of "test it like a user" gets past that. If it is isolated, then either the isolation needs a documented way through for automation, or the flow is yours to cover and the contract should say so plainly — because right now each side can reasonably assume the other has it. **Unrelated signature seen in the same run, reported without diagnosis** (78 occurrences, none fatal — the scenarios fail on the cap refusal above, not on this): `call_sdk Deserialization error of config Error: invalid type: JsValue(Function), expected any valid JSON value`. It appears only once `sharedWallet` is configured and `ensureIdentity()` actually opens a wallet — it was absent from every earlier run. The consumer passes `sharedWallet` as three plain strings, per `## Surface`, so whatever crosses that boundary as a function does not come from its call site. A second, smaller consequence of the same hole: the consumer's `currentUserId` now has to be read out of its own profile document, so it is empty until that read lands, where it used to be available synchronously and invariant. An action taken in that window is silently dropped instead of written. ## 5. "Permissive in" is stated as a guarantee but is not uniform `## Guarantees` opens with *"Every entry accepts `NuriLike` and validates at the door"*, and makes a point of it: a value read from storage, a URL or a form goes straight in, no guard to call, no cast to write. The signature block does not honour it uniformly — `storeRegistry.openDocumentInbox(doc: Nuri)` takes the precise type where its own deposit-side counterpart `inbox.readForDocument(doc: NuriLike)` takes the permissive one. Verified by the consumer's typechecker, not read from your source. Either the guarantee is narrower than stated (say which entries it covers), or `openDocumentInbox` should widen. As written, a consumer that trusts the sentence gets a type error at exactly one call site and has no way to tell whether that is the rule or the exception.