82be3684fa
Le commentaire affirmait qu'aucune des deux valeurs ne permet de prendre une lecture a zero ligne pour une absence definitive. `unknown` le permet, et deliberement : `index-deposit.noRowsMeansAbsent` l'accepte, parce que la ou il n'y a pas de semantique `State` il n'y a pas de barriere a avoir manquee. Les deux valeurs ne sont pas interchangeables, et laquelle est juste depend de la page - ce que l'assertion epingle dans les deux mondes est la meme chose : pas `"synced"`.
187 lines
8.7 KiB
TypeScript
187 lines
8.7 KiB
TypeScript
/**
|
|
* cold-read-for-document.test.ts — coming back to the messages left on my note.
|
|
*
|
|
* ── The gap this closes ───────────────────────────────────────────────────
|
|
* The inbox surface offered a read with the SYNC GUARANTEE (`readSynced`) and a read
|
|
* ADDRESSED BY DOCUMENT (`readForDocument`), and not their intersection. An application
|
|
* materializing deposits needs both, so it had to resolve an inbox address itself — the
|
|
* one gesture the contract says an application never performs.
|
|
*
|
|
* ── Why the document-addressed path needs the barrier TWICE ───────────────
|
|
* Reading a document's messages crosses two repos: the DOCUMENT, whose Header branch
|
|
* carries the address, and the INBOX, which carries the deposits. On a fresh session over
|
|
* the same persistent wallet both are present and unsynced, and an anchored read of an
|
|
* unsynced repo returns no rows — no error (`emulated-verifier/open-repo.ts`). So the
|
|
* ADDRESS read comes back empty, `readForDocument` concludes "this document has no inbox",
|
|
* and answers `[]` for a note whose inbox holds the message somebody left on it.
|
|
*
|
|
* That is the state this suite starts from, reached the way a real page reaches it: Alice
|
|
* writes a note and opens it for messages, Bob leaves one, and Alice comes back on a new
|
|
* page. Nothing is planted — a second page sees exactly what the first one WROTE, and the
|
|
* broker fake only withholds what this page has not subscribed to yet
|
|
* (`wallet-fake.ts`, `unsyncedUntilSubscribed`).
|
|
*
|
|
* The pair of tests is the point: on ONE state, the ungated read answers empty and the
|
|
* gated one answers the message. A test that only showed `readSyncedForDocument` returning
|
|
* deposits would pass just as well over a plain `read`.
|
|
*/
|
|
|
|
import { test, expect, describe, afterAll, beforeEach } from "bun:test";
|
|
import { docs, inbox as inboxSurface, storeRegistry } from "../src/index";
|
|
import { getSyncState } from "../src/emulated-verifier/open-repo";
|
|
import { listMyEntityDocs } from "../src/shared-wallet/account-registry";
|
|
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 broker's own cold start — see `wallet-fake.WalletOptions`. */
|
|
const COLD = { unsyncedUntilSubscribed: true } as const;
|
|
|
|
/**
|
|
* The first visit, in the application's own vocabulary: Alice writes a note and opens it
|
|
* for messages; Bob leaves one on it. Both name the NOTE and nothing else.
|
|
*
|
|
* Returns the note, which is all an application ever holds.
|
|
*/
|
|
async function aNoteWithAMessageOnIt(quads: Quad[]): Promise<Nuri> {
|
|
bootPage(quads, COLD);
|
|
await signIn("alice");
|
|
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
|
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 the note was opened on, read off the WALLET — the emulated `AddInboxCap`
|
|
* record. The test asks the wallet because no application can ask the package: there is
|
|
* deliberately no published call that hands out an address, which is the whole reason
|
|
* `readSyncedForDocument` has to exist.
|
|
*/
|
|
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 by listing her own store.
|
|
*
|
|
* That listing is the LIBRARY's own path now, not an application's: `listMyEntityDocs`
|
|
* stopped being published in `1.0.0-dev.5` (no counterpart at any level upstream), so
|
|
* this reaches it where it lives. What is under test is unchanged - the cold store still
|
|
* has to answer, and it still has to refile the caps while it does. */
|
|
async function myNote(): Promise<Nuri> {
|
|
const mine = await listMyEntityDocs("alice", "public");
|
|
const note = mine[0];
|
|
if (!note) throw new Error("the note Alice wrote is not in her store");
|
|
return note;
|
|
}
|
|
|
|
/** Alice comes back on a NEW page, over the wallet the first one wrote. */
|
|
async function aliceComesBack(quads: Quad[]): Promise<Nuri> {
|
|
reloadPage(quads, COLD);
|
|
await signIn("alice");
|
|
return myNote();
|
|
}
|
|
|
|
beforeEach(() => {
|
|
forgetEverything();
|
|
});
|
|
|
|
afterAll(() => {
|
|
forgetEverything();
|
|
});
|
|
|
|
describe("reading the messages left on my note, on a page that has just loaded", () => {
|
|
test("the ungated document-addressed read answers EMPTY — the note's repo never synced", async () => {
|
|
const quads: Quad[] = [];
|
|
await aNoteWithAMessageOnIt(quads);
|
|
const note = await aliceComesBack(quads);
|
|
|
|
// Not a failure anyone can see: the message is on the broker, Alice owns the inbox,
|
|
// and the call returns a perfectly ordinary empty list.
|
|
expect(await inboxSurface.readForDocument(note)).toEqual([]);
|
|
// ...because nothing on THIS page ever brought the NOTE into view. The address lives
|
|
// on it.
|
|
//
|
|
// `"timed-out"`, not `"unknown"`, since 2026-09-24: a reset DISOWNS the documents it takes
|
|
// subscriptions away from rather than forgetting them, so `"unknown"` keeps its one
|
|
// meaning - "no barrier was ever expected here".
|
|
//
|
|
// CORRECTED 2026-09-24, and the reason that used to stand here was FALSE. It said neither
|
|
// value lets a zero-row read be called a definitive absence: `unknown` does, and
|
|
// deliberately - `emulated-verifier/index-deposit.ts` `noRowsMeansAbsent` returns true for
|
|
// it, because where there are no `State` semantics there is no barrier to have missed. The
|
|
// two values are not interchangeable, and which one is correct depends on the page. This
|
|
// particular one is a harness artefact: `reloadPage` resets the registry inside ONE process,
|
|
// so the note is still known to it from the first page, and `"timed-out"` - "opened here,
|
|
// and no barrier to show for it" - is the exact truth. A browser reload starts with an empty
|
|
// module, where nothing was ever opened and `"unknown"` is the exact truth instead. What
|
|
// this line pins in both worlds is the same thing: not `"synced"`.
|
|
expect(getSyncState(note)).toBe("timed-out");
|
|
});
|
|
|
|
test("the synced document-addressed read answers the message, over that same state", async () => {
|
|
const quads: Quad[] = [];
|
|
await aNoteWithAMessageOnIt(quads);
|
|
const note = await aliceComesBack(quads);
|
|
|
|
const mine = await inboxSurface.readSyncedForDocument(note);
|
|
expect(mine.map((d) => (d.payload as { text: string }).text)).toEqual([MESSAGE]);
|
|
expect(mine.map((d) => d.from)).toEqual(["bob"]);
|
|
});
|
|
|
|
test("it crosses the sync barrier on BOTH repos the answer depends on", async () => {
|
|
const quads: Quad[] = [];
|
|
const written = await aNoteWithAMessageOnIt(quads);
|
|
const inbox = inboxOnTheNote(quads, written);
|
|
const note = await aliceComesBack(quads);
|
|
|
|
await inboxSurface.readSyncedForDocument(note);
|
|
|
|
// The guarantee itself, not the payload: past the first `State` on each, presence is
|
|
// guaranteed and absence definitive — so an empty answer would MEAN empty. The note's
|
|
// barrier is the one this call adds (nothing else on the page opens a note); the
|
|
// inbox's is `readSynced`'s, and connecting may have crossed it already.
|
|
expect(getSyncState(note)).toBe("synced");
|
|
expect(getSyncState(inbox)).toBe("synced");
|
|
});
|
|
|
|
test("a document nobody opened an inbox on answers empty, not an error", async () => {
|
|
const quads: Quad[] = [];
|
|
bootPage(quads, COLD);
|
|
await signIn("alice");
|
|
const bare = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
|
|
|
reloadPage(quads, COLD);
|
|
await signIn("alice");
|
|
expect(await inboxSurface.readSyncedForDocument(bare)).toEqual([]);
|
|
});
|
|
|
|
test("it is still a read of MY inbox — the owner's guard is not bypassed", async () => {
|
|
const quads: Quad[] = [];
|
|
await aNoteWithAMessageOnIt(quads);
|
|
const note = await aliceComesBack(quads);
|
|
|
|
// Bob can find where to deposit for Alice's public note, and that is all: reading it
|
|
// would collect the caps addressed to her. A second door onto the same read must not
|
|
// be a way around the guard the first one carries.
|
|
await signIn("bob");
|
|
await expect(inboxSurface.readSyncedForDocument(note)).rejects.toThrow(
|
|
/does not belong to the connected wallet/i,
|
|
);
|
|
});
|
|
});
|