fix: à la reconnexion, on retrouve ce qu'on possède — pas seulement ce qu'on a reçu
Une application signalait deux symptômes. Ils sont indépendants, et ils ont une racine commune : se connecter ne rejouait qu'UN des registres durables du portefeuille. connectedUser lisait les AddLink de la branche User — ce qu'on vous a partagé — et rien d'autre. Les capacités des documents que vous avez FAITS vivent sur la branche Store, et un seul chemin les relisait : listMyEntityDocs. Une application qui recharge et va droit à sa note ne tenait donc rien pour elle. Constaté : capFor(note) vaut undefined juste après une connexion résolue, et devient défini dès que listMyEntityDocs passe. Le public survivait en lecture parce que fetchReadCap va rechercher la clé dans le store ; il échouait quand même à l'écriture, qui ne fait pas cette démarche. Deuxième défaut : myInboxes énumérait les inbox sans en remettre la clé au détenteur. Se connecter demandait donc un document qu'on n'avait pas de quoi lire — sur une inbox, que l'application n'a jamais nommée puisque rien ne le lui permet. Troisième défaut, et c'est lui qui rendait tout ça fatal : un drainage refusé faisait échouer toute la connexion. Une inbox n'étant pas consommée par un échec, elle refusait à chaque tentative suivante. D'où le « trois fois sur trois », et d'où un verrouillage plutôt qu'un partage manquant. C'est ma spécification qui l'a créé. En rendant les échecs visibles j'avais écrasé une distinction : ne pas ATTEINDRE la file est une panne, et refuser la session est juste ; ne pas pouvoir APPLIQUER un élément est une donnée, et ça ne doit priver personne de sa session. Le drainage par inbox est désormais isolé — signalé haut et fort, les autres files drainées, la session accordée — tandis qu'énumérer les files et restaurer rejettent toujours. Le parcours e2e ratait les deux : personne ne se reconnecte après avoir ouvert son document aux messages. Il s'arrêtait une reconnexion trop tôt.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* reload-inbox-drain.test.ts — signing in again, over an inbox that was opened last time.
|
||||
*
|
||||
* ── The report this reproduces ────────────────────────────────────────────
|
||||
* *"`ensureIdentity()` rejects inside its own inbox processing, about a document the
|
||||
* application never named. The application deposits through a published call; the failure
|
||||
* happens afterwards, inside the package."*
|
||||
*
|
||||
* The document nobody named is an INBOX. An application opens one on a note it owns
|
||||
* (`storeRegistry.openDocumentInbox`) and hands out nothing: the deposit side names the
|
||||
* NOTE (`inbox.postToDocument`) and the read side names it too (`inbox.readForDocument`).
|
||||
* There is deliberately no published way to ask for an address, so the inbox document is
|
||||
* the package's from end to end.
|
||||
*
|
||||
* Connecting drains every inbox the user may read — its own two, plus one per document it
|
||||
* opened one on, read back from the User branch (the emulated `AddInboxCap`). Reading an
|
||||
* inbox is a guarded read like any other, so a session that does not hold the inbox's key
|
||||
* cannot drain it. And that key reaches the owner's hands exactly once, when the inbox is
|
||||
* MINTED; nothing puts it back on a later session.
|
||||
*
|
||||
* So the second connection asks for a document it may not touch, is refused, and the
|
||||
* refusal comes back out of `ensureIdentity()` — not as an empty screen but as a rejected
|
||||
* sign-in. It will be rejected at every future connection too, because an inbox that could
|
||||
* not be drained is still on the list next time.
|
||||
*
|
||||
* ── Why the applicative journey never saw it ──────────────────────────────
|
||||
* `e2e/notebook.ts` journey 3 has Alice open her note for messages and Bob deposit into
|
||||
* it; Bob reopens the application, Alice never does. The journey stops one reconnection
|
||||
* short of the state this suite starts from.
|
||||
*/
|
||||
|
||||
import { test, expect, describe, afterAll, beforeEach } from "bun:test";
|
||||
import { docs, inbox as inboxSurface, storeRegistry } from "../src/index";
|
||||
import { getCaps } from "../src/shared-wallet/bootstrap";
|
||||
import { runScheduledInboxProcessingNow } from "../src/emulated-verifier/inbox-processor";
|
||||
import { bootPage, forgetEverything, reloadPage, signIn, SESSION, type Quad } from "./wallet-fake";
|
||||
import type { Nuri } from "../src/model/types";
|
||||
|
||||
const TITLE = "urn:test:title";
|
||||
const MESSAGE = "j'apporte le café";
|
||||
|
||||
/**
|
||||
* The first visit, in the application's own vocabulary: Alice writes a note and opens it
|
||||
* for messages; Bob leaves one on it. Both of them name the NOTE and nothing else.
|
||||
*
|
||||
* Returns the note, which is all an application ever holds — the inbox NURI is read off
|
||||
* the wallet afterwards, by the test alone, to say WHICH document the refusal is about.
|
||||
*/
|
||||
async function aNoteOpenForMessages(quads: Quad[]): Promise<Nuri> {
|
||||
bootPage(quads);
|
||||
await signIn("alice");
|
||||
const note = await storeRegistry.createEntityDoc("public");
|
||||
await docs.sparqlUpdate(
|
||||
SESSION.sessionId,
|
||||
`INSERT DATA { <${note}> <${TITLE}> "Courses" }`,
|
||||
note,
|
||||
"writeEntity",
|
||||
);
|
||||
await storeRegistry.openDocumentInbox(note);
|
||||
|
||||
await signIn("bob");
|
||||
await inboxSurface.postToDocument(note, { payload: { text: MESSAGE }, from: "bob", ts: 1 });
|
||||
return note;
|
||||
}
|
||||
|
||||
/**
|
||||
* The inbox Alice's note was opened on, read off the WALLET — the emulated `AddInboxCap`
|
||||
* record, which is where the connection's drain list comes from. The test asks the wallet
|
||||
* because no application can ask the package.
|
||||
*/
|
||||
function inboxOnTheNote(quads: Quad[], note: Nuri): Nuri {
|
||||
const record = quads.find((q) => q.p === "urn:ng-eventually:shim:inboxCap" && q.o.startsWith(note + " "));
|
||||
if (!record) throw new Error("no AddInboxCap record was written for the note");
|
||||
return record.o.split(" ")[1] as Nuri;
|
||||
}
|
||||
|
||||
/** Alice's note, found the way her application finds it: by listing her own store. */
|
||||
async function myNote(): Promise<Nuri> {
|
||||
const mine = await storeRegistry.listMyEntityDocs("public");
|
||||
const note = mine[0];
|
||||
if (!note) throw new Error("the note Alice wrote is not in her store");
|
||||
return note;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
forgetEverything();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
forgetEverything();
|
||||
});
|
||||
|
||||
describe("signing in again, after opening one of my documents for messages", () => {
|
||||
test("the connection work `ensureIdentity()` awaits does not reject", async () => {
|
||||
const quads: Quad[] = [];
|
||||
await aNoteOpenForMessages(quads);
|
||||
|
||||
reloadPage(quads);
|
||||
await signIn("alice");
|
||||
});
|
||||
|
||||
test("the reconnected session holds the key of the inbox it is asked to drain", async () => {
|
||||
const quads: Quad[] = [];
|
||||
const note = await aNoteOpenForMessages(quads);
|
||||
const inbox = inboxOnTheNote(quads, note);
|
||||
|
||||
reloadPage(quads);
|
||||
await signIn("alice").catch(() => undefined);
|
||||
|
||||
expect(getCaps().capFor(inbox)).toBeDefined();
|
||||
});
|
||||
|
||||
test("the message left on my note is there when I come back", async () => {
|
||||
const quads: Quad[] = [];
|
||||
await aNoteOpenForMessages(quads);
|
||||
|
||||
reloadPage(quads);
|
||||
await signIn("alice").catch(() => undefined);
|
||||
|
||||
const mine = await inboxSurface.readForDocument(await myNote());
|
||||
expect(mine.map((d) => (d.payload as { text: string }).text)).toEqual([MESSAGE]);
|
||||
});
|
||||
|
||||
// The deferred drain runs under whoever is connected — Bob, here — and reports to the
|
||||
// access log rather than to a caller. It must not decide whether the owner can sign in.
|
||||
test("a deferred drain having run first does not change the owner's sign-in", async () => {
|
||||
const quads: Quad[] = [];
|
||||
await aNoteOpenForMessages(quads);
|
||||
await runScheduledInboxProcessingNow();
|
||||
|
||||
reloadPage(quads);
|
||||
await signIn("alice");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user