fix: la suite n'était pas hermétique, et deux tests ne pouvaient pas échouer
Second tour adverse sur le lot D. Trois trouvailles, et une erreur de diagnostic de ma part qui vaut d'être consignée. **La suite verte dépendait de l'ordre des fichiers.** `bun test isolation-active public-store` donnait 5 échecs quand chaque fichier seul était vert — donc un checkout de CI avec un autre ordre d'inodes livrait rouge. Deux causes distinctes : - le travail de connexion, lancé sans être attendu par `setCurrentUser`, débordait d'un fichier sur le suivant et armait l'émulation. `connectedUser` abandonne désormais dès que l'identité pour laquelle il a démarré n'est plus connectée — ce qui est de toute façon la bonne sémantique : en amont une session appartient à un utilisateur, et changer d'utilisateur est une autre session ; - et surtout **mon propre test de store public exposait le cap d'un document que personne ne détient** — un état que la bibliothèque ne produit jamais. Il ne passait que tant que l'émulation était désarmée. Alice crée sa note avant de l'exposer, maintenant. Balayage des 21 paires de fichiers : plus aucune ne pollue. **Le contrôle symétrique ajouté hier ne pouvait pas échouer.** « La liste d'Alice ne contient pas la note de Bob » lisait un rendu ANTÉRIEUR à l'écriture de Bob : l'attente de `showScope` était satisfaite au premier sondage par le marqueur déjà à l'écran, sans synchroniser quoi que ce soit. Alice écrit désormais une note APRÈS celle de Bob — `writeNote` attend son apparition, donc ce qui suit est un rendu qui post-date. Et le `.catch` qui avalait le délai d'attente est retiré : une liste qui ne se stabilise jamais est un échec à voir, pas une dégradation à absorber. **Le test anti-fork prouvait « pas le premier », pas « le canonique ».** Son minimum lexicographique était aussi le DERNIER élément, si bien qu'un choix positionnel — la faute exacte que ce test existe pour attraper — restait vert. Le minimum est déplacé au milieu ; vérifié par mutation, « prendre le dernier » le fait rougir. **Mon erreur de diagnostic.** J'ai cru trouver, sous la trouvaille d'ordre, une fuite entre utilisateurs — les caps d'Alice classés chez Bob — et je l'ai « reproduite ». Le repro était faux : son faux `ng` ignorait le sujet dans la requête d'inbox, donc l'inbox de Bob résolvait vers celle d'Alice. Une fois le faux corrigé, la fuite ne se reproduit plus, ni avec ni sans correctif. Le danger reste réel en lecture du code — trois chemins classent des caps plusieurs `await` après la garde qui les autorisait — donc `caps.holderKey`/`learnFor` le ferment par construction, mais les commentaires disent maintenant ce que c'est : un risque fermé, pas un défaut observé. 189 tests unitaires, e2e 40/40 et applicatif 12/12.
This commit is contained in:
@@ -209,8 +209,12 @@ describe("deterministic resolution over a doc-shim corrupted by fork residue", (
|
||||
// Seed the pointer (store-root → doc-shim) and the corrupted record IN the doc-shim.
|
||||
fakeNg._quads.push({ g: ROOT, s: "urn:ng-eventually:shim:root", p: "urn:ng-eventually:shim:shimDoc", o: docShim });
|
||||
const subj = "urn:ng-eventually:shim:account:dupuser";
|
||||
// The minimum must sit NEITHER first NOR last, or the test cannot tell a canonical
|
||||
// pick from a positional one. It used to end on `pub-a`, so `rows[rows.length - 1]`
|
||||
// — an order-dependent pick, precisely the fault this test exists to catch — passed
|
||||
// it. Only `rows[0]` failed. Found by mutation, 2026-08-10.
|
||||
const dupPublics = [
|
||||
"did:ng:o:pub-m", "did:ng:o:pub-a", "did:ng:o:pub-z", "did:ng:o:pub-c", "did:ng:o:pub-a",
|
||||
"did:ng:o:pub-m", "did:ng:o:pub-a", "did:ng:o:pub-z", "did:ng:o:pub-c", "did:ng:o:pub-z",
|
||||
];
|
||||
fakeNg._quads.push({ g: docShim, s: subj, p: "urn:ng-eventually:shim:id", o: "dupuser" });
|
||||
for (const p of dupPublics)
|
||||
|
||||
@@ -55,6 +55,21 @@ afterEach(() => {
|
||||
setCurrentUser(null);
|
||||
});
|
||||
|
||||
/**
|
||||
* Alice creates her note and exposes its cap — the two halves of what `createEntityDoc`
|
||||
* does for a `public` scope, in that order.
|
||||
*
|
||||
* The `mint` is not decoration: exposing writes to the document, and writing needs to
|
||||
* reach it. Without it this file only passed while the emulation happened to be
|
||||
* DISARMED, which made its results depend on which test file ran first — it went red in
|
||||
* `bun test <other-file> test/public-store.test.ts`. A fixture that exposes a cap for a
|
||||
* document nobody holds describes a state the library never produces.
|
||||
*/
|
||||
async function aliceExposesHerNote(): Promise<void> {
|
||||
getCaps().mint(PUB);
|
||||
await exposeReadCap(PUB, mintCap(PUB));
|
||||
}
|
||||
|
||||
/** Arm the emulation without giving the current holder anything: some OTHER document. */
|
||||
function armEmulation(): void {
|
||||
setCurrentUser("someone-else");
|
||||
@@ -66,7 +81,7 @@ const PUB = "did:ng:o:pub" as Nuri;
|
||||
test("a cap exposed on a document is downloaded by a holder that has nothing", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
await exposeReadCap(PUB, mintCap(PUB));
|
||||
await aliceExposesHerNote();
|
||||
|
||||
setCurrentUser("bob");
|
||||
armEmulation();
|
||||
@@ -91,7 +106,7 @@ test("a document that exposes nothing yields nothing — that is the normal case
|
||||
test("a cap naming ANOTHER document is refused, not filed", async () => {
|
||||
const { quads } = inject();
|
||||
setCurrentUser("alice");
|
||||
await exposeReadCap(PUB, mintCap(PUB));
|
||||
await aliceExposesHerNote();
|
||||
// Forge the exposed value so it names a different document.
|
||||
quads[0]!.o = mintCap("did:ng:o:someone-elses" as Nuri);
|
||||
|
||||
@@ -116,7 +131,7 @@ test("inert while no cap has been issued at all — nothing to obtain, nothing a
|
||||
test("a public store serves every asker, not only the first", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
await exposeReadCap(PUB, mintCap(PUB));
|
||||
await aliceExposesHerNote();
|
||||
armEmulation();
|
||||
|
||||
setCurrentUser("bob");
|
||||
@@ -131,7 +146,7 @@ test("a public store serves every asker, not only the first", async () => {
|
||||
test("asked once per document: the outcome is memoised, in both directions", async () => {
|
||||
const { sparql_query } = inject();
|
||||
setCurrentUser("alice");
|
||||
await exposeReadCap(PUB, mintCap(PUB));
|
||||
await aliceExposesHerNote();
|
||||
armEmulation();
|
||||
setCurrentUser("bob");
|
||||
|
||||
@@ -150,7 +165,7 @@ test("asked once per document: the outcome is memoised, in both directions", asy
|
||||
test("resetting the caps forgets the memo — a stale yes would hand back what is no longer held", async () => {
|
||||
const { sparql_query } = inject();
|
||||
setCurrentUser("alice");
|
||||
await exposeReadCap(PUB, mintCap(PUB));
|
||||
await aliceExposesHerNote();
|
||||
armEmulation();
|
||||
setCurrentUser("bob");
|
||||
await fetchReadCap(PUB);
|
||||
|
||||
Reference in New Issue
Block a user