diff --git a/packages/polyfill/e2e/notebook.ts b/packages/polyfill/e2e/notebook.ts index a51ab5a..442b1aa 100644 --- a/packages/polyfill/e2e/notebook.ts +++ b/packages/polyfill/e2e/notebook.ts @@ -192,7 +192,7 @@ const SIGN_IN_MS = enclosingBound([NEW_PAGE_MS, BROKER_ROUND_TRIP_MS, FIRST_REND const JOURNEY_MS = 4 * 60 * 1000; /** * The batch's wall clock — the last resort behind every bound above, for the wait nobody - * wrapped. A healthy run measures 3.2 min; seven journeys at their own bound would be far + * wrapped. A healthy run measures 3.2 min; eight journeys at their own bound would be far * more than this, and that is intended: this is not the sum of the journeys, it is the point * past which a run has stopped being a measurement of anything. What it replaces is a run * killed by hand at 68 minutes having printed nothing. @@ -237,6 +237,15 @@ const SUITE: readonly JourneyDeclaration[] = [ name: "Bob leaves a message on Alice's note, and only Alice reads it", checks: ["Alice reads the message left on her note"], }, + { + name: "Alice comes back after opening her note for messages, and it is all still hers", + checks: [ + "coming back is granted, and the application knows her again", + "she goes straight to the note she made and reads it, holding only its reference", + "she can still WRITE to a note she made before she came back", + "the message left while she was away is waiting for her", + ], + }, { name: "each actor's list holds their own notes, and no one else's", checks: [ @@ -813,7 +822,100 @@ async function main(): Promise { }, }); - // 4. Each actor lists their OWN notes and nothing else — the boundary, seen from + // 4. The reconnection nobody made. Every journey above stops one reconnection short: + // Alice opens her note to messages and it is always somebody ELSE who reopens. So + // what an owner holds on a FRESH session over the same durable wallet was never + // looked at, and two defects shipped behind that gap — both reported by a consuming + // application, neither visible here (fixed 2026-08-16, `f6d1734`): + // + // - connecting replayed only the register of what was SHARED WITH you (the User + // branch's Links) and never the one holding the keys of the documents you MADE + // (the Store branch). A creator who came back and went straight to their own + // note held nothing for it: a public one still READ, because its store hands + // the key to whoever asks, and the WRITE was refused — which is why this + // journey does both, and why a read alone would have missed the defect; + // - and the inbox opened on a document was enumerated at connection without its + // key, so draining it failed — and a failed drain took the whole sign-in down + // with it. An inbox is not consumed by failing, so it refused again at every + // later attempt: not a delayed share, a person locked out for good. + // + // Hence the shape: an owner reaches a state only an owner reaches — a document of + // her own, open to messages, holding somebody else's deposit — and only THEN comes + // back. Nothing is seeded: the reconnection is a new page over the same wallet, + // which is what a person's reload produces, and everything she finds afterwards + // she finds through the application. + await journey({ + name: "Alice comes back after opening her note for messages, and it is all still hers", + needs: [aliceIsUp, bobIsUp], + run: async () => { + const before = await must(ALICE, alice); + // TWO notes, and the second one is not a spare: `openDocumentInbox` is idempotent, + // so opening the FIRST one again after coming back would return early and write + // nothing. A note she has not yet opened is what makes "she can still write to a + // document she made" an actual write rather than a lookup. + await writeNote(before, "public", "Boîte à idées", "laissez vos suggestions"); + const ideasRef = await referenceOnScreen(before, "Boîte à idées"); + await writeNote(before, "public", "Recettes", "tarte aux pommes"); + await openForMessages(before, "Boîte à idées"); + + // Bob reopens so the address published on the note is visible to his session, then + // deposits by naming the NOTE — the published call, no inbox in sight. The + // reference is the one value that crosses, off Alice's screen, as in journey 1. + bob = await reopen(ctx!, url, await must(BOB, bob)); + await leaveMessage(bob, ideasRef, "et si on ajoutait un index ?"); + + // THE reconnection this journey exists for. A new page over the same durable + // wallet: every module-level cache the library holds is gone, and what she finds + // is what connecting restored. `reopen` signs the new page in before closing the + // old one, so a refusal here leaves Alice exactly as she was. + alice = await reopen(ctx!, url, before); + const back = await must(ALICE, alice); + // Granted at all — which is the whole of the second defect. Pre-fix her undrainable + // queue rejected `ensureIdentity()`, the application never rendered, and this + // journey died on the sign-in with none of its checks reported. + const who = ((await back.frame.locator('[data-testid="who"]').textContent()) ?? "").trim(); + check("coming back is granted, and the application knows her again", who.includes(ALICE), who); + + // Straight to her note, by its reference — a bookmark, a deep link — and + // DELIBERATELY before anything lists her public shelf: listing a scope re-reads its + // Store branch and puts those keys back, so a read taken after one proves only that + // the listing healed it. + const mine = await openReceivedNote(back, ideasRef); + check( + "she goes straight to the note she made and reads it, holding only its reference", + mine.includes("Boîte à idées") && mine.includes("laissez vos suggestions"), + mine, + ); + + await showScope(back, "public", "Recettes"); + // `showScope` settles on an EMPTY list too, and a page this fresh can render one + // before its repos have synchronised — so the note gets its own bounded wait. Not + // swallowed here, unlike journey 7: what follows is a CLICK on that note, and a + // click on an absent one would report Playwright's own default rather than the + // named step that actually waited. + await measured("a note from an earlier visit reappearing", WRITE_NOTE_MS, (ms) => + back.frame.locator('li:has-text("Recettes")').waitFor({ timeout: ms }), + ); + // The WRITE — a statement anchored on a document she created in the session before + // this one, which is the act her own key is needed for and the one a public store + // does not cover. + await openForMessages(back, "Recettes"); + // Read off the screen rather than asserted as a literal `true`: what this journey is + // entitled to claim is what the application SAYS happened, and a detail a reader can + // compare against a failing run is worth more than a constant. + const said = ((await back.frame.locator('[data-testid="share-result"]').textContent()) ?? "").trim(); + check("she can still WRITE to a note she made before she came back", said.includes("ouverte aux messages"), said); + + const left = await readMessages(back, "Boîte à idées"); + check( + "the message left while she was away is waiting for her", + left.includes("et si on ajoutait un index ?"), + left, + ); + }, + }); + + // 5. Each actor lists their OWN notes and nothing else — the boundary, seen from // the only place that matters: what the screen shows. await journey({ name: "each actor's list holds their own notes, and no one else's", @@ -842,7 +944,7 @@ async function main(): Promise { }, }); - // 5. The path no journey walked: somebody who holds NOTHING. No wallet in the + // 6. The path no journey walked: somebody who holds NOTHING. No wallet in the // profile, no identifier anywhere, and the application's own address — not the // broker redirect `signIn()` goes through, which loads the application already // inside the iframe and so never meets the barrier. Two defects shipped green @@ -982,7 +1084,7 @@ async function main(): Promise { }, }); - // 6. The visit AFTER the first one, on the application's own address. The barrier used + // 7. The visit AFTER the first one, on the application's own address. The barrier used // to skip itself here — it asked only when nobody was known — and skipping is // silent: the page goes straight to the broker, and someone whose browser no longer // holds the wallet lands on a static dead end with no return path. The wallet is