test: quatre tests prouvaient autre chose que ce qu'ils annonçaient
Lot D de la revue adverse. Aucun changement de comportement de la bibliothèque : ce sont
les tests qui mentaient, et deux faux `ng` qui fabriquaient un état que le vrai broker ne
produit pas.
**« Un tiers résout l'inbox d'un autre utilisateur » prouvait le CACHE.** `userInbox`
indexe par (compte, portée) sans regarder qui demande, donc Bob tombait sur l'entrée que
la session d'Alice venait de chauffer. Rien de la persistance n'était exercé — le faux ne
servait même pas la requête `docInbox` — si bien que dans une seconde SESSION, ou une
seconde page de navigateur comme en pilote la suite applicative, Bob aurait obtenu une
inbox DIFFÉRENTE et son dépôt serait parti où personne ne lit. C'est la panne que cette
bibliothèque a déjà payée une fois.
Deux causes, toutes deux dans les faux : la requête `docInbox` n'était servie nulle part,
et le faux de `cross-user-access` prenait le **nom de graphe** pour le sujet dans un
`INSERT DATA { GRAPH <g> { … } }` — donc le pointeur du shim n'était jamais retrouvé et
chaque résolution à froid créait un nouveau shim. Les deux corrigées, et les tests qui
franchissent une frontière d'identité purgent maintenant le cache à la frontière.
**Mon propre index d'inbox avait le même défaut**, découvert en faisant ça : `isKnownInbox`
ne répondait que par sa mémoire, parce qu'aucun faux ne servait la requête. La moitié
durable n'était pas exercée — exactement la faute que ce lot corrigeait ailleurs.
**« Connecting drains BOTH levels » n'observait pas le second niveau.** Rejoué contre un
`connectedUser` qui ne draine que les inbox de l'utilisateur, il restait vert. La raison
n'est pas un test faible : le second niveau n'a **aucun producteur**. Le seul appel qui
dépose un cap est `inbox.share(doc, toUser)`, qui résout l'inbox d'un UTILISATEUR, jamais
celle d'un document. Drainer une inbox de document n'applique donc rien. Le test dit
désormais ce qu'il prouve, et l'anticipation est nommée comme telle : en amont
`AddInboxCap` est générique sur les repos et `InboxMsgContent::Link` existe, donc viser
cela est légitime — annoncer que c'est exercé ne l'était pas.
**« La liste de Bob ne contient pas la note d'Alice » n'avait pas de contrôle positif.**
Bob n'écrivait jamais de note publique : sa liste était vide quoi qu'il arrive. Il en
écrit une maintenant, et la vérification symétrique est ajoutée. Au passage, `showScope`
lisait le DOM avant le rendu — le gestionnaire `change` de l'application lance
`refresh()` sans l'attendre.
189 tests unitaires, e2e 40/40 et applicatif 12/12.
This commit is contained in:
@@ -116,10 +116,25 @@ async function signIn(ctx: BrowserContext, appUrl: string, id: string): Promise<
|
||||
|
||||
// ── the acts, expressed as the application expresses them ───────────────────
|
||||
|
||||
/** Show the notes of `scope` — the list is per-scope, so acting on a note means
|
||||
* looking at the right shelf first. */
|
||||
async function showScope(a: Actor, scope: string): Promise<void> {
|
||||
/**
|
||||
* Show the notes of `scope` — the list is per-scope, so acting on a note means looking at
|
||||
* the right shelf first.
|
||||
*
|
||||
* The wait is not decoration: the application's `change` handler runs `void refresh()`,
|
||||
* un-awaited, so reading `textContent` straight after `selectOption` reads the PREVIOUS
|
||||
* shelf. A suite that asserts "Bob's list does not contain Alice's note" against a list
|
||||
* that has not re-rendered is green whether isolation holds or not — found adversarially,
|
||||
* 2026-08-10.
|
||||
*/
|
||||
async function showScope(a: Actor, scope: string, settle = "les notes"): Promise<void> {
|
||||
await a.frame.locator('[data-testid="scope"]').selectOption(scope);
|
||||
// The list is rebuilt wholesale; waiting for the marker the caller expects (or for the
|
||||
// list to be empty) is the only signal the application offers.
|
||||
await a.frame
|
||||
.locator(`[data-testid="notes"]:has-text("${settle}"), [data-testid="notes"]:empty`)
|
||||
.first()
|
||||
.waitFor({ timeout: 60000 })
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async function writeNote(a: Actor, scope: string, title: string, body: string): Promise<void> {
|
||||
@@ -243,7 +258,7 @@ async function main(): Promise<void> {
|
||||
// 3. A note opened for messages: anyone deposits, only its owner reads. Bob addresses
|
||||
// the NOTE — he never names an inbox, and no application should have to.
|
||||
await journey("Bob leaves a message on Alice's note, and only Alice reads it", async () => {
|
||||
await showScope(alice, "public"); // her public shelf, where "Courses" lives
|
||||
await showScope(alice, "public", "Courses"); // her public shelf
|
||||
await openForMessages(alice, "Courses");
|
||||
// Bob has to REOPEN so the address published on the note is visible to his session.
|
||||
const bob2 = await reopen(ctx!, url, bob);
|
||||
@@ -257,13 +272,21 @@ async function main(): Promise<void> {
|
||||
// 4. Each actor lists their OWN notes and nothing else — the boundary, seen from
|
||||
// the only place that matters: what the screen shows.
|
||||
await journey("each actor's list holds their own notes, and no one else's", async () => {
|
||||
await showScope(alice, "public");
|
||||
// POSITIVE CONTROL. Bob writes a public note of his own first — without it his list
|
||||
// is empty whatever the boundary does, and "it does not contain Alice's note" is
|
||||
// true for the wrong reason. The assertion has to be able to fail.
|
||||
await writeNote(bob, "public", "Vélo", "réviser les freins");
|
||||
await showScope(bob, "public", "Vélo");
|
||||
const bobList = (await bob.frame.locator('[data-testid="notes"]').textContent()) ?? "";
|
||||
|
||||
await showScope(alice, "public", "Courses");
|
||||
await alice.frame.locator('li:has-text("Courses")').waitFor({ timeout: 60000 });
|
||||
const aliceList = (await alice.frame.locator('[data-testid="notes"]').textContent()) ?? "";
|
||||
await showScope(bob, "public");
|
||||
const bobList = (await bob.frame.locator('[data-testid="notes"]').textContent()) ?? "";
|
||||
check("Alice sees her own note", aliceList.includes("Courses"), aliceList.slice(0, 80));
|
||||
check("Bob's own list does not contain Alice's note", !bobList.includes("Courses"), bobList.slice(0, 80) || "(vide)");
|
||||
|
||||
check("Alice sees her own note", aliceList.includes("Courses"), aliceList.slice(0, 60));
|
||||
check("Bob sees HIS own note — the control that lets the next check fail", bobList.includes("Vélo"), bobList.slice(0, 60));
|
||||
check("Bob's list does not contain Alice's note", !bobList.includes("Courses"), bobList.slice(0, 60));
|
||||
check("Alice's list does not contain Bob's note", !aliceList.includes("Vélo"), aliceList.slice(0, 60));
|
||||
});
|
||||
} finally {
|
||||
await ctx?.close().catch(() => {});
|
||||
|
||||
Reference in New Issue
Block a user