Files
ng-eventually/packages/polyfill/test/document-inbox-surfaces-failure.test.ts
Sylvain Duchesne 75313f75a2 fix!: une lecture qui n'atteint pas son document echoue, elle ne repond plus vide
`fetchReadCap` rendait `false` pour trois situations sans rapport : l'emulation
inerte, le document qui n'expose aucune capacite, et le reseau qu'on n'a pas pu
interroger. Les trois appelants vivants lisaient ce `false` comme « pas de
capacite » et transformaient une panne en affirmation sur le document :
`ensureRepoOpen` renoncait en silence et laissait la lecture suivante accuser le
lecteur de ne rien detenir, et `documentInboxAddress` repondait « ce document n'a
pas d'inbox », ce qu'une application lit comme zero depot.

Le telechargement rejette desormais jusqu'a l'appelant, avec le document nomme et
la cause d'origine conservee. `false` ne veut plus dire qu'une chose : le document
n'expose rien, et c'est une reponse. Le memo garde la meme asymetrie qu'avant.

`documentInboxAddress` propage aussi l'echec de sa propre lecture d'adresse : le
`catch` qui rendait `undefined` se justifiait par un refus que `mustNotAttempt` a
deja ecarte une ligne plus haut, donc il n'avalait que des pannes.

`useShapeQuery` franchit la barriere sur chaque document NOMME par `scope.graphs`
avant d'ouvrir la requete : capacite demandee au store public, refus a voix haute
si elle n'est pas detenue, ouverture et synchronisation. Un document nomme
qu'on ne peut pas lire donnait `isSuccess` avec `data: []` apres que le filtre de
capacites l'eut vide - la seule reponse que cette surface existe pour reserver a
« ce scope ne contient rien ». Restent intacts et succes : `did:ng:i`, `graphs:
[]`, un document joignable et vide, et tout le cas inerte.

`fetchReadCaps` supprime : aucun appelant, et une politique de tolerance par lot
sans proprietaire au-dessus d'un appel qui rejette desormais.
2026-09-21 15:00:01 +02:00

102 lines
5.2 KiB
TypeScript

/**
* document-inbox-surfaces-failure.test.ts - "this document has no inbox" must be an ANSWER.
*
* -- The defect this pins, and why it is worse than it looks ---------------
* `documentInboxAddress` reads a document's Header branch for the address to deposit at,
* and answers `undefined` when there is none. Two published calls turn that value into a
* statement an application acts on: `inbox.postToDocument` says "its owner never opened
* one", and `inbox.readForDocument` says zero deposits.
*
* Until `1.0.0-dev.9` it answered `undefined` for two things that are not that. The public
* store could not be asked for the document's cap, and `fetchReadCap` reported a breakdown
* with the same `false` a document exposing no cap answers with; or the address read itself
* broke, and a `catch` returned `undefined`. Either way a depositor was told the document
* has no inbox, and a reader was handed an empty list - for a document whose inbox is full.
* That is the failure-as-absence this package has closed roughly eight times.
*
* -- What is under test, and what must NOT move ---------------------------
* The pair is the point, and neither half proves anything alone. A document nobody opened
* an inbox on still answers empty, and still without an error: **an absence is an answer**,
* and a repair that turned it into a failure would only have moved the lie. The tests below
* run BOTH over the same harness, so one cannot be satisfied at the other's expense.
*
* -- The fault is the broker's --------------------------------------------
* Nothing here reaches into the library to make one of its functions throw. The world is
* built through the surface - Alice writes a public note and opens it for messages - and
* then the fake broker stops answering for one named repo (`wallet-fake._failReadsOn`),
* which is what `resolve_target_for_sparql` does upstream for a repo the verifier does not
* hold: it rejects, and never answers 0 rows.
*/
import { test, expect, describe, afterAll, beforeEach } from "bun:test";
import { inbox as inboxSurface, storeRegistry } from "../src/index";
import { bootPage, forgetEverything, signIn, type Quad } from "./wallet-fake";
import type { Nuri } from "../src/model/types";
/** The broker's own cold start, so the barrier is a real one here too. */
const COLD = { unsyncedUntilSubscribed: true } as const;
beforeEach(() => {
forgetEverything();
});
afterAll(() => {
forgetEverything();
});
describe("a document's inbox address says nothing it does not know", () => {
test("a broker that cannot answer for the document is a REJECTION, not 'no inbox'", async () => {
const quads: Quad[] = [];
const fake = bootPage(quads, COLD);
await signIn("alice");
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
await storeRegistry.openDocumentInbox(note);
// Bob holds nothing: the public store is what would serve him the note's cap, and it
// is exactly that round-trip the broker now cannot answer.
await signIn("bob");
fake._failReadsOn.add(note);
// Before `1.0.0-dev.9` this resolved to `undefined` and `postToDocument` turned it
// into "its owner never opened one" - a statement about Alice's note, made out of a
// broker outage. Bob would have gone away believing there was nowhere to write.
await expect(
inboxSurface.postToDocument(note, { payload: { text: "j'apporte le cafe" } }),
).rejects.toThrow(/UNKNOWN/);
});
test("the reader is told too: a failed address read does not read as zero deposits", async () => {
const quads: Quad[] = [];
const fake = bootPage(quads, COLD);
await signIn("alice");
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
await storeRegistry.openDocumentInbox(note);
// Alice's own page, so the cap question never arises and `fetchReadCap` is a memo hit:
// what breaks is the ADDRESS read itself, which used to be caught and answered
// `undefined`. This is the second of the two routes, and it needs its own test because
// repairing the first one leaves it untouched.
fake._failReadsOn.add(note);
await expect(inboxSurface.readForDocument(note)).rejects.toThrow(/RepoNotFound/);
await expect(inboxSurface.readSyncedForDocument(note)).rejects.toThrow(/RepoNotFound/);
});
test("and a document nobody opened an inbox on still answers EMPTY, and succeeds", async () => {
const quads: Quad[] = [];
bootPage(quads, COLD);
await signIn("alice");
// Created through the surface and never opened for messages: a genuine absence, on a
// broker answering perfectly well. This must stay a success, or the repair above
// would have swapped one lie for another.
const bare: Nuri = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
expect(await inboxSurface.readForDocument(bare)).toEqual([]);
expect(await inboxSurface.readSyncedForDocument(bare)).toEqual([]);
// And the deposit side says the same thing in its own words - a refusal that names
// the real state, not an outage.
await expect(
inboxSurface.postToDocument(bare, { payload: { text: "hello" } }),
).rejects.toThrow(/has no inbox/);
});
});