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:
@@ -126,21 +126,32 @@ async function signIn(ctx: BrowserContext, appUrl: string, id: string): Promise<
|
||||
* 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> {
|
||||
async function showScope(a: Actor, scope: string, settle: string): 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.
|
||||
//
|
||||
// This wait is NOT a synchronisation point when the marker is ALREADY on screen — it
|
||||
// matches on the first poll and returns before the in-flight `refresh()` has done its
|
||||
// broker round-trips. A check reading the list right after is then reading the previous
|
||||
// render. Where a journey needs a FRESH list, it must create its own synchronisation
|
||||
// point (a write it awaits), not lean on this. Found adversarially, 2026-08-10.
|
||||
//
|
||||
// No `.catch` swallowing the timeout either: a list that never settles is a failure to
|
||||
// see, not a degradation to absorb — swallowing it reinstated the very bug this wait
|
||||
// was added to fix.
|
||||
await a.frame
|
||||
.locator(`[data-testid="notes"]:has-text("${settle}"), [data-testid="notes"]:empty`)
|
||||
.first()
|
||||
.waitFor({ timeout: 60000 })
|
||||
.catch(() => {});
|
||||
.waitFor({ timeout: 60000 });
|
||||
}
|
||||
|
||||
async function writeNote(a: Actor, scope: string, title: string, body: string): Promise<void> {
|
||||
await a.frame.locator('[data-testid="title"]').fill(title);
|
||||
await a.frame.locator('[data-testid="body"]').fill(body);
|
||||
await showScope(a, scope);
|
||||
// No settle marker to wait for here: the write below is its own synchronisation point,
|
||||
// and the shelf we are switching to may legitimately be empty or hold anything.
|
||||
await a.frame.locator('[data-testid="scope"]').selectOption(scope);
|
||||
await a.frame.locator('[data-testid="write"]').click();
|
||||
await a.frame.locator(`li:has-text("${title}")`).waitFor({ timeout: 60000 });
|
||||
}
|
||||
@@ -279,11 +290,14 @@ async function main(): Promise<void> {
|
||||
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 });
|
||||
// Alice's list has to be re-rendered AFTER Bob's note exists, or "she does not see
|
||||
// it" is read off a stale snapshot and holds whatever the boundary does. Writing a
|
||||
// note is the synchronisation point the application offers: `writeNote` awaits the
|
||||
// new entry appearing, so what follows is a render that post-dates Bob's.
|
||||
await writeNote(alice, "public", "Timbres", "en acheter un carnet");
|
||||
const aliceList = (await alice.frame.locator('[data-testid="notes"]').textContent()) ?? "";
|
||||
|
||||
check("Alice sees her own note", aliceList.includes("Courses"), aliceList.slice(0, 60));
|
||||
check("Alice sees her own notes", aliceList.includes("Courses") && aliceList.includes("Timbres"), 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));
|
||||
|
||||
Reference in New Issue
Block a user