diff --git a/.project/concepts/nextgraph-platform/decision_2026-06-17_eventually-library.md b/.project/concepts/nextgraph-platform/decision_2026-06-17_eventually-library.md index 6df7f27..3456ee9 100644 --- a/.project/concepts/nextgraph-platform/decision_2026-06-17_eventually-library.md +++ b/.project/concepts/nextgraph-platform/decision_2026-06-17_eventually-library.md @@ -24,7 +24,7 @@ Tout le polyfill qui compense l'immaturité de NextGraph (pas de lecture cross-w ### Comment les mécanismes tranchés s'y logent - **Identité / login** : le client fixe l'utilisateur courant (username en polyfill ; wallet en cible — [[decision_2026-06-15_shared-wallet-login-flow]]). -- **Droits d'accès** : **capabilities émulées** comme données (grants attachés aux documents), enforcées **génériquement** par le client. L'app **attache les grants** via des opérations de cap anticipées (créer public, accorder à une connexion…) — **comme en cible**. Aucune politique n'est injectée ; seuls les shapes et les *actes* d'attribution viennent du consommateur. +- **Droits d'accès** : **ReadCap émulées** dans un registre **par DOCUMENT** (`CapRegistry` : qui détient la read/write-cap de chaque NURI ; docs publics lisibles sans cap), enforcées **génériquement** par le client. L'unité d'accès est le **document = le `@graph`** de l'item, **jamais l'item** — fidèle au modèle vérifié ([[knowledge_stores-permissions]] : un store est un repo conteneur ; détenir la cap du store ne donne PAS celles des repos qu'il référence ; pas d'héritage de lecture). En mono-store (tout dans un repo) le filtre est donc **tout-ou-rien** sur ce document → la granularité fine **exige 1 document par entité**. L'app **ouvre/accorde les caps** via des opérations anticipées (`open(doc, scope, owner)`, `grantRead`, `makePublic`) — **comme en cible**. Aucune politique n'est injectée ; seuls les shapes et les *actes* d'attribution viennent du consommateur. - **Inbox** : `inbox.post(...)` (signature anticipée) côté client ; **matérialisation** par un **curateur** (package séparé, **différé**). Mécanisme réutilisé pour l'inscription PdR **et** la soumission à l'index. - **Découverte** : index **alimenté via son inbox** ([[decision_2026-06-16_discovery-model]]). Le client **dépose** (inbox) + **lit** (abonnement) ; un **curateur** matérialise. Le **propriétaire cible** de l'index reste à décider (app singleton ?, incertain — [[knowledge_apps-and-services]]). - **Synchronisation** : `s'abonner à un document` (natif). En polyfill, wallet partagé ⇒ sync multi-device native entre sessions. @@ -35,7 +35,7 @@ Tout le polyfill qui compense l'immaturité de NextGraph (pas de lecture cross-w ## Conséquences - **Festipod ne dépend que de `@ng-eventually/client`** ; la complexité du polyfill est invisible côté app ; rien de Festipod dans la lib. -- **Migration** : retirer l'alias de build + l'appel de bootstrap → le client redevient le vrai SDK ; **traduire les grants émulés en vraies caps** (étape de données). Le **mécanisme cible de l'index global** reste à décider (app singleton ?, [[knowledge_apps-and-services]]) — ce n'est **pas** un backend. Le code applicatif ne bouge pas. +- **Migration** : retirer l'alias de build + l'appel de bootstrap → le client redevient le vrai SDK ; **traduire les ReadCap émulées (registre par document) en vraies caps NextGraph** (étape de données). Le **mécanisme cible de l'index global** reste à décider (app singleton ?, [[knowledge_apps-and-services]]) — ce n'est **pas** un backend. Le code applicatif ne bouge pas. - Le [[brief_2026-06-15_shared-wallet-shim]] décrit désormais **comment Festipod consomme `ng-eventually`** (les mécanismes y sont *réalisés par la lib*), plus une implémentation interne à l'app. ## Statut d'intégration (2026-06-25) @@ -51,10 +51,10 @@ Tout le polyfill qui compense l'immaturité de NextGraph (pas de lecture cross-w - **Point d'injection unique (option 1)** : dans l'app, **seul `ngSession`** importe le vrai SDK au runtime — uniquement pour `configure(...)`. Tout le reste de l'app (data, lifecycle, login, types) passe par la lib. - **Pourquoi pas « lib importe le SDK elle-même »** : la lib étant dans un **repo séparé** (arbre `node_modules` distinct), si elle importait `@ng-org` au runtime, le bundle aurait **deux copies** d'`@ng-org` → l'ORM (signaux mono-instance) casserait. L'injection garantit **un seul exemplaire** (celui de Festipod). *(Le « zéro accès direct » exigerait la lib en workspace dans le repo — écarté pour la garder externe ; cf. options 2/3 discutées.)* - **Exceptions assumées** (hors « app ») : `src/shared/test-harness/auth-setup.tsx` (bootstrap wallet de test) et `src/shared/test-harness/harness.tsx` (harness **mock**, `deepSignal`) gardent un import direct `@ng-org`. Les **bindings ORM générés** (`festipodShapes.*`) aussi (types générés). -- **Filtre de lecture — IMPLÉMENTÉ & validé (2026-06-25)** : `read-filter.ts` — `makeReadFilteredView` (un **Proxy** sur le set réactif : itération/`size`/`forEach` filtrés par `canRead(grant, utilisateur)`, mutations forwardées) + `filterReadable` (pur). `useShape` l'applique **uniquement si un `grantOf` est configuré** (sinon passthrough → pas de régression). Grant = donnée portée par le document (résolveur `grantOf` injecté ; domaine-agnostique). Validé : **4 tests unitaires** (logique + Proxy + utilisateur dynamique) **et un scénario `@data`** qui l'exerce sur le **vrai `DeepSignalSet`** contre le broker (filtre actif → seules les participations de l'utilisateur ciblé). *Piège rencontré : ne pas cibler `td.currentUserId` (vide au build du bridge) — viser un vrai utilisateur des données.* -- **Validé (global)** : build Festipod · `@ui` 4/4 · **`@data` 9/9 (47 steps) contre le broker** · lib (typecheck + **8 tests**). +- **Filtre ReadCap — IMPLÉMENTÉ & validé (2026-06-29, refactor du modèle grant→ReadCap)** : `caps.ts` — `CapRegistry` (read/write-cap **par document NURI** + docs publics ; `open/grantRead/grantWrite/makePublic/canRead/canWrite/governsRead/hasReadPolicy`). `read-filter.ts` — `makeReadFilteredView` (un **Proxy** sur le set réactif : itération/`size`/`forEach` gardés par `caps.canRead(item['@graph'], utilisateur)` ; un item sans `@graph` ou dans un document non gouverné est conservé ; mutations forwardées) + `filterReadable` (pur). `useShape` l'applique **uniquement si `caps.hasReadPolicy()`** (sinon passthrough → pas de régression). **Plus de `grantOf` injecté** : le filtre lit l'`@graph` et consulte le registre — automatique et domaine-agnostique. Validé : **6 tests `caps` + 4 tests `read-filter`** (logique + Proxy + utilisateur dynamique + non-héritage entre documents) **et un scénario `@data`** sur le **vrai `DeepSignalSet`** contre le broker : on gouverne le document du wallet par une ReadCap accordée à un autre utilisateur → l'utilisateur courant voit **0** ; il obtient la cap → il voit **toutes** les participations (tout-ou-rien en mono-store, fidèle). +- **Validé (global, 2026-06-29)** : `@data` ReadCap 5/5 steps contre le broker · lib (typecheck `rc=0` + **10 tests**). *(2 échecs e2e préexistants « J'y serai » = libellé obsolète depuis le portage redesign 5a29938, hors périmètre — l'app ne déclare aucune cap, `useShape` reste en passthrough.)* -Reste à implémenter dans la lib (stubs `TODO`, nécessitent la couche comptes/grants pour être *actifs* dans l'app) : **garde d'écriture**, **`inbox.post`** + matérialisation, **login wallet partagé**. +Reste à implémenter dans la lib (stubs `TODO`, nécessitent la couche comptes/caps pour être *actifs* dans l'app) : **garde d'écriture** (`caps.canWrite` est prêt côté registre), **`inbox.post`** + matérialisation, **login wallet partagé**. ## Open Questions diff --git a/.project/concepts/nextgraph-platform/knowledge_stores-permissions.md b/.project/concepts/nextgraph-platform/knowledge_stores-permissions.md index edfa407..9e67000 100644 --- a/.project/concepts/nextgraph-platform/knowledge_stores-permissions.md +++ b/.project/concepts/nextgraph-platform/knowledge_stores-permissions.md @@ -35,9 +35,13 @@ Tout wallet a d'office les **3 stores** private/protected/public (session : `pri ## Concepts transverses -**Document vs Repo.** *« A Repo is the equivalent of an E2EE group for one and only one Document. »* **1 document = 1 repo** (commits + permissions). Identifiant : `did:ng:o:`. Un **store** est lui-même un document spécial qui regroupe et permissionne d'autres documents. +**Document vs Repo.** *« A Repo is the equivalent of an E2EE group for one and only one Document. »* **1 document = 1 repo** (commits + permissions). Identifiant : `did:ng:o:`. **Il n'existe pas de type `Document`** dans le code (`nextgraph-rs`, vérifié 2026-06-29) : « document » = **un repo quelconque**. Un **store est un repo spécial** (`is_store=true`, avec branches `Store`/`Overlay`/`User`) — donc *un store est un document, mais un document n'est pas forcément un store*. -**Granularité.** Écriture gérée au niveau **Document (repo)**, pas branche/bloc. Lecture plus fine possible (par bloc/branche). Héritage : un Group store peut faire hériter ses permissions à ses documents. +**Containment (store → repos) par RÉFÉRENCE, pas par liste.** Un store **ne contient pas** un `Vec` : il référence ses repos via un **graphe RDF** dans sa branche Overlay/User. À l'inverse, chaque repo déclare son store parent via `RootBranchV0.store: StoreOverlay` (`engine/repo/src/types.rs`) → **un repo appartient à exactement un store**. C'est la « structure de graphe » : un store **peut contenir d'autres documents**. + +**Granularité des caps.** `ReadCap = ObjectRef`. Granularité au niveau **repo ET branche** (chaque branche a son `read_cap`), jusqu'au **bloc** (clé `ObjectKey`/ChaCha20). Écriture gérée au niveau **Document (repo)**. + +**Pas d'héritage de lecture automatique.** Détenir la ReadCap d'un **store** ne donne **pas** accès aux repos qu'il contient — **il faut la ReadCap de chaque repo**. L'héritage optionnel `inherit_perms_users_and_quorum_from_store: Option` ne partage que les **users/quorum** (écriture/permissions), **pas** la possession de read-cap. (Repos d'un private_store : héritage implicite.) **Conséquence pour l'émulation** : l'unité d'accès en lecture est le **repo = le `@graph`** de chaque item — un filtre par document, pas par store ni par item (cf. [[decision_2026-06-17_eventually-library]]). **Capability / Nuri.** Le partage transmet un **Nuri** embarquant la capability crypto (lecture et/ou écriture). Pas d'ACL centralisée : posséder le Nuri = le droit. *« adding permissions can be done offline »* ; *« removing permissions … requires a SyncSignature »* (synchrone). diff --git a/src/modules/workshop/features/read-filter.feature b/src/modules/workshop/features/read-filter.feature index 18a6e69..804a8ae 100644 --- a/src/modules/workshop/features/read-filter.feature +++ b/src/modules/workshop/features/read-filter.feature @@ -1,14 +1,17 @@ # language: fr @WORKSHOP @priority-1 -Fonctionnalité: Filtre de lecture (ng-eventually) +Fonctionnalité: Filtre ReadCap (ng-eventually) En tant que développeur Je veux valider, contre le vrai broker, que le filtre de lecture de la lib - ne renvoie que les données autorisées pour l'utilisateur courant, sur le vrai - set réactif de l'ORM. + applique les ReadCap au niveau du DOCUMENT (le repo où vit chaque item) sur le + vrai set réactif de l'ORM : on ne voit un document que si on détient sa ReadCap. + En mono-store (tout dans un seul repo) c'est donc tout-ou-rien sur ce document + — le comportement fidèle de NextGraph. @data - Scénario: Le filtre ne renvoie que les participations autorisées - Étant donné le wallet contient des participations de plusieurs utilisateurs - Quand j'active le filtre de lecture pour l'utilisateur courant - Alors je ne vois que les participations de l'utilisateur courant - Et le filtre a masqué au moins une participation d'un autre utilisateur + Scénario: On ne voit un document que si on détient sa ReadCap + Étant donné le wallet contient des participations dans un document + Quand je gouverne ce document par une ReadCap accordée à un autre utilisateur + Alors l'utilisateur courant ne voit aucune participation de ce document + Quand l'utilisateur courant obtient la ReadCap du document + Alors il voit toutes les participations du document diff --git a/src/modules/workshop/steps/data/read-filter.steps.ts b/src/modules/workshop/steps/data/read-filter.steps.ts index 62b23a1..aeccd10 100644 --- a/src/modules/workshop/steps/data/read-filter.steps.ts +++ b/src/modules/workshop/steps/data/read-filter.steps.ts @@ -2,45 +2,45 @@ import { Given, When, Then } from '@cucumber/cucumber'; import { expect } from 'chai'; import type { FestipodWorld } from '../../../../shared/support/world'; -// Validates ng-eventually's READ FILTER on the REAL ORM set, against the broker. -// The harness grants each participation to its own `user`; with the filter on -// for a chosen user, useShape must yield only that user's participations. +// Validates ng-eventually's READ FILTER (ReadCap) on the REAL ORM set, against +// the broker. The filter is per-DOCUMENT (an item's @graph = the repo it lives +// in): you see a document only if you hold its read cap. In mono-store, every +// participation shares one document, so governing it is all-or-nothing — the +// faithful NextGraph behavior. Two synthetic users discriminate cap possession, +// NOT the participation's own `user` field. -Given('le wallet contient des participations de plusieurs utilisateurs', async function (this: FestipodWorld) { - // Deterministic: create two participations for two synthetic users on a - // synthetic event (isolated from real-event counts; joinEvent is idempotent on - // event+user, so this doesn't accumulate across runs). +Given('le wallet contient des participations dans un document', async function (this: FestipodWorld) { + // Deterministic: ensure ≥1 participation exists in the wallet document. + // joinEvent is idempotent on (event,user), so this doesn't accumulate. await this.appFrame!.evaluate(async () => { const td = (window as any).__testData; - await td.joinEvent('urn:rf:event', 'urn:rf:alice'); - await td.joinEvent('urn:rf:event', 'urn:rf:bob'); + await td.joinEvent('urn:rf:event', 'urn:rf:p1'); + await td.joinEvent('urn:rf:event', 'urn:rf:p2'); }); await this.appFrame!.waitForFunction( () => { const ps = [...(window as any).__testData.participations]; - return ps.some((p: any) => p.user === 'urn:rf:alice') && ps.some((p: any) => p.user === 'urn:rf:bob'); + return ps.some((p: any) => p.user === 'urn:rf:p1') && ps.some((p: any) => p.user === 'urn:rf:p2'); }, null, { timeout: 15000 }, ); const data = await this.appFrame!.evaluate(() => { const td = (window as any).__testData; - const parts = [...td.participations]; - const targetUser = 'urn:rf:alice'; - return { - targetUser, - total: parts.length, - ownCount: parts.filter((p: any) => p.user === targetUser).length, - }; + // Raw set (no policy yet) → true total in the document. + return { total: [...td.participations].length, documentNuri: td.documentNuri }; }); - (this as any).rf = data; - expect(data.ownCount, 'exactly one participation for the target user').to.equal(1); - expect(data.total, 'other users have participations too').to.be.greaterThan(data.ownCount); + (this as any).rf = { ...data, reader: 'urn:rf:alice', other: 'urn:rf:bob' }; + expect(data.total, 'the document holds participations').to.be.greaterThan(0); }); -When('j\'active le filtre de lecture pour l\'utilisateur courant', async function (this: FestipodWorld) { - const { targetUser } = (this as any).rf; - await this.appFrame!.evaluate((u: string) => (window as any).__testData.setupReadFilter(u), targetUser); +When('je gouverne ce document par une ReadCap accordée à un autre utilisateur', async function (this: FestipodWorld) { + const { reader, other } = (this as any).rf; + // Grant the document's read cap to `reader`; current user is `other` (no cap). + await this.appFrame!.evaluate( + (args: { reader: string; user: string }) => (window as any).__testData.governDocument(args.reader, args.user), + { reader, user: other }, + ); await this.appFrame!.waitForFunction( () => (window as any).__readFilter?.ready === true, null, @@ -48,15 +48,18 @@ When('j\'active le filtre de lecture pour l\'utilisateur courant', async functio ); }); -Then('je ne vois que les participations de l\'utilisateur courant', async function (this: FestipodWorld) { - const r = await this.appFrame!.evaluate(() => (window as any).__readFilter); - const { targetUser, ownCount } = (this as any).rf; - expect(r.users.every((u: string) => u === targetUser), 'all filtered participations belong to the target user').to.be.true; - expect(r.count, 'filtered count equals the target user own participations').to.equal(ownCount); +Then('l\'utilisateur courant ne voit aucune participation de ce document', async function (this: FestipodWorld) { + const snap = await this.appFrame!.evaluate(() => (window as any).__readFilter.snapshot()); + expect(snap.count, 'a user without the read cap sees nothing of the document').to.equal(0); }); -Then('le filtre a masqué au moins une participation d\'un autre utilisateur', async function (this: FestipodWorld) { - const r = await this.appFrame!.evaluate(() => (window as any).__readFilter); - const { total } = (this as any).rf; - expect(total, 'the filter hid at least one other-user participation').to.be.greaterThan(r.count); +When('l\'utilisateur courant obtient la ReadCap du document', async function (this: FestipodWorld) { + const { reader } = (this as any).rf; + await this.appFrame!.evaluate((u: string) => (window as any).__testData.setUser(u), reader); +}); + +Then('il voit toutes les participations du document', async function (this: FestipodWorld) { + const { total } = (this as any).rf; + const snap = await this.appFrame!.evaluate(() => (window as any).__readFilter.snapshot()); + expect(snap.count, 'the cap holder sees every participation of the document').to.equal(total); }); diff --git a/src/shared/data/features.ts b/src/shared/data/features.ts index dea78e9..6fe7aae 100644 --- a/src/shared/data/features.ts +++ b/src/shared/data/features.ts @@ -356,7 +356,7 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Alors", - "text": "l'écran contient une section \"Relayer l'événement\"" + "text": "l'écran contient un bouton \"Suivant\"" } ] }, @@ -374,48 +374,6 @@ export const parsedFeatures: ParsedFeature[] = [ } ] }, - { - "name": "Détecter un événement similaire déjà relayé", - "tags": [], - "steps": [ - { - "keyword": "Étant donné que ", - "text": "l'écran \"create-event\" est affiché" - }, - { - "keyword": "Alors", - "text": "le formulaire permet de détecter les doublons" - } - ] - }, - { - "name": "Importer un événement depuis une source externe", - "tags": [], - "steps": [ - { - "keyword": "Étant donné que ", - "text": "l'écran \"create-event\" est affiché" - }, - { - "keyword": "Alors", - "text": "le formulaire permet d'importer depuis Mobilizon ou Transiscope" - } - ] - }, - { - "name": "Pas d'alerte doublon lors d'un import externe", - "tags": [], - "steps": [ - { - "keyword": "Étant donné que ", - "text": "l'écran \"create-event\" est affiché" - }, - { - "keyword": "Alors", - "text": "l'import externe ne déclenche pas d'alerte doublon" - } - ] - }, { "name": "Modifier un événement", "tags": [], @@ -433,7 +391,7 @@ export const parsedFeatures: ParsedFeature[] = [ } ], "filePath": "src/modules/event/features/us-13-creer-evenement.feature", - "rawContent": "# language: fr\n@EVENT @priority-1\nFonctionnalité: US-13 Relayer/Modifier/Supprimer un événement\n En tant qu'utilisateur\n Je peux relayer/modifier/supprimer un événement\n En choisissant les dates, horaires, lieu et thématique\n Afin de relayer/présenter le contenu de cet événement et le catégoriser\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder au formulaire de relai d'événement\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"relayer un événement\"\n Alors je vois l'écran \"create-event\"\n\n Scénario: Vérifier les champs obligatoires du formulaire\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire contient les champs obligatoires suivants:\n | Nom de l'événement |\n | Date de début |\n | Heure de début |\n | Lieu |\n | Thématique |\n\n Scénario: Vérifier la présence du bouton de relai\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors l'écran contient une section \"Relayer l'événement\"\n\n Scénario: Pouvoir annuler le relai d'événement\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors je peux annuler et revenir à l'écran précédent\n\n Scénario: Détecter un événement similaire déjà relayé\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire permet de détecter les doublons\n\n Scénario: Importer un événement depuis une source externe\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire permet d'importer depuis Mobilizon ou Transiscope\n\n Scénario: Pas d'alerte doublon lors d'un import externe\n Étant donné que l'écran \"create-event\" est affiché\n Alors l'import externe ne déclenche pas d'alerte doublon\n\n Scénario: Modifier un événement\n * Scénario non implémenté\n\n Scénario: Supprimer un événement\n * Scénario non implémenté\n\n Scénario: Retirer une organisation (personne ou structure)\n * Scénario non implémenté\n", + "rawContent": "# language: fr\n@EVENT @priority-1\nFonctionnalité: US-13 Relayer/Modifier/Supprimer un événement\n En tant qu'utilisateur\n Je peux relayer/modifier/supprimer un événement\n En choisissant les dates, horaires, lieu et thématique\n Afin de relayer/présenter le contenu de cet événement et le catégoriser\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder au formulaire de relai d'événement\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"relayer un événement\"\n Alors je vois l'écran \"create-event\"\n\n Scénario: Vérifier les champs obligatoires du formulaire\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire contient les champs obligatoires suivants:\n | Nom de l'événement |\n | Date de début |\n\n Scénario: Vérifier la présence du bouton de relai\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors l'écran contient un bouton \"Suivant\"\n\n Scénario: Pouvoir annuler le relai d'événement\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors je peux annuler et revenir à l'écran précédent\n\n Scénario: Modifier un événement\n * Scénario non implémenté\n\n Scénario: Supprimer un événement\n * Scénario non implémenté\n\n Scénario: Retirer une organisation (personne ou structure)\n * Scénario non implémenté\n", "screenIds": [ "create-event", "home" @@ -523,21 +481,17 @@ export const parsedFeatures: ParsedFeature[] = [ "background": [ { "keyword": "Étant donné que ", - "text": "l'utilisateur a chargé les données de test" + "text": "le portefeuille contient des données de test" } ], "scenarios": [ { - "name": "Créer un événement depuis l'accueil", + "name": "Créer un événement et vérifier qu'il apparaît sur l'accueil", "tags": [], "steps": [ { "keyword": "Quand", - "text": "l'utilisateur navigue vers l'écran \"home\"" - }, - { - "keyword": "Et", - "text": "l'utilisateur clique sur le bouton \"Relayer un événement\"" + "text": "l'utilisateur navigue vers l'écran \"create-event\"" }, { "keyword": "Et", @@ -554,13 +508,7 @@ export const parsedFeatures: ParsedFeature[] = [ { "keyword": "Et", "text": "l'écran contient le texte \"Pique-nique au parc\"" - } - ] - }, - { - "name": "L'événement créé apparaît sur l'accueil", - "tags": [], - "steps": [ + }, { "keyword": "Quand", "text": "l'utilisateur navigue vers l'écran \"home\"" @@ -575,13 +523,9 @@ export const parsedFeatures: ParsedFeature[] = [ "name": "L'événement créé persiste après reconnexion", "tags": [], "steps": [ - { - "keyword": "Quand", - "text": "l'utilisateur navigue vers l'écran \"home\"" - }, { "keyword": "Alors", - "text": "l'écran contient le texte \"Pique-nique au parc\"" + "text": "l'écran d'accueil contient le texte \"Pique-nique au parc\"" } ] }, @@ -591,11 +535,7 @@ export const parsedFeatures: ParsedFeature[] = [ "steps": [ { "keyword": "Quand", - "text": "l'utilisateur navigue vers l'écran \"home\"" - }, - { - "keyword": "Et", - "text": "l'utilisateur clique sur l'événement \"Pique-nique au parc\"" + "text": "l'utilisateur clique sur un événement de l'accueil" }, { "keyword": "Alors", @@ -603,16 +543,12 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Et", - "text": "l'écran contient le texte \"Pique-nique au parc\"" - }, - { - "keyword": "Et", - "text": "l'écran contient le texte \"Parc Bordelais\"" + "text": "l'écran contient le texte \"Participants\"" } ] }, { - "name": "S'inscrire à un événement existant", + "name": "S'inscrire à un événement", "tags": [], "steps": [ { @@ -621,15 +557,19 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Et", - "text": "l'utilisateur clique sur le premier événement" + "text": "l'utilisateur clique sur un événement de la liste" }, { "keyword": "Et", - "text": "l'utilisateur clique sur le bouton \"Participer\"" + "text": "l'utilisateur attend que l'écran \"event-detail\" soit affiché" + }, + { + "keyword": "Et", + "text": "l'utilisateur clique sur le bouton \"J'y serai\" si visible" }, { "keyword": "Alors", - "text": "l'écran contient le texte \"Inscrit\"" + "text": "l'écran contient le texte \"Je participe\"" } ] }, @@ -643,20 +583,46 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Et", - "text": "l'utilisateur clique sur le premier événement" + "text": "l'utilisateur clique sur un événement de la liste" }, { "keyword": "Et", - "text": "l'utilisateur clique sur le bouton \"Inscrit\"" + "text": "l'utilisateur attend que l'écran \"event-detail\" soit affiché" + }, + { + "keyword": "Et", + "text": "l'utilisateur clique sur le bouton \"Je participe\"" }, { "keyword": "Alors", - "text": "l'écran contient le texte \"Participer\"" + "text": "l'écran contient le texte \"J'y serai\"" } ] }, { - "name": "Modifier le titre d'un événement créé", + "name": "La désinscription persiste après reconnexion", + "tags": [], + "steps": [ + { + "keyword": "Quand", + "text": "l'utilisateur navigue vers l'écran \"events\"" + }, + { + "keyword": "Et", + "text": "l'utilisateur clique sur un événement de la liste" + }, + { + "keyword": "Et", + "text": "l'utilisateur attend que l'écran \"event-detail\" soit affiché" + }, + { + "keyword": "Alors", + "text": "l'écran contient le texte \"J'y serai\"" + } + ] + }, + { + "name": "Modifier un événement et vérifier la persistance", "tags": [], "steps": [ { @@ -665,7 +631,11 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Et", - "text": "l'utilisateur clique sur l'événement \"Pique-nique au parc\"" + "text": "l'utilisateur clique sur un événement de l'accueil" + }, + { + "keyword": "Et", + "text": "l'utilisateur attend que l'écran \"event-detail\" soit affiché" }, { "keyword": "Et", @@ -673,7 +643,11 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Et", - "text": "l'utilisateur modifie le champ \"Nom de l'événement\" avec \"Pique-nique d'été\"" + "text": "l'utilisateur attend que l'écran \"update-event\" soit affiché" + }, + { + "keyword": "Et", + "text": "l'utilisateur modifie le champ lieu avec \"Jardin Public, Bordeaux\"" }, { "keyword": "Et", @@ -685,31 +659,13 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Et", - "text": "l'écran contient le texte \"Pique-nique d'été\"" - } - ] - }, - { - "name": "La modification persiste après reconnexion", - "tags": [], - "steps": [ - { - "keyword": "Quand", - "text": "l'utilisateur navigue vers l'écran \"home\"" - }, - { - "keyword": "Alors", - "text": "l'écran contient le texte \"Pique-nique d'été\"" - }, - { - "keyword": "Et", - "text": "l'écran ne contient pas le texte \"Pique-nique au parc\"" + "text": "l'écran contient le texte \"Jardin Public\"" } ] } ], "filePath": "src/modules/event/features/cycle-de-vie-evenement.feature", - "rawContent": "# language: fr\n@EVENT @priority-1\nFonctionnalité: Cycle de vie d'un événement\n En tant qu'utilisateur connecté\n Je peux créer, consulter, modifier et participer à des événements\n Et ces actions persistent dans mon portefeuille NextGraph\n\n Contexte:\n Étant donné que l'utilisateur a chargé les données de test\n\n # --- Création ---\n\n @e2e\n Scénario: Créer un événement depuis l'accueil\n Quand l'utilisateur navigue vers l'écran \"home\"\n Et l'utilisateur clique sur le bouton \"Relayer un événement\"\n Et l'utilisateur remplit le formulaire de création d'événement:\n | champ | valeur |\n | Nom de l'événement | Pique-nique au parc |\n | Date de début | 2026-06-15 |\n | Heure de début | 14:00 |\n | Lieu | Parc Bordelais, Bordeaux |\n Et l'utilisateur clique sur le bouton \"Relayer l'événement\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Pique-nique au parc\"\n\n @e2e\n Scénario: L'événement créé apparaît sur l'accueil\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran contient le texte \"Pique-nique au parc\"\n\n @e2e\n Scénario: L'événement créé persiste après reconnexion\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran contient le texte \"Pique-nique au parc\"\n\n # --- Consultation ---\n\n @e2e\n Scénario: Consulter le détail d'un événement depuis l'accueil\n Quand l'utilisateur navigue vers l'écran \"home\"\n Et l'utilisateur clique sur l'événement \"Pique-nique au parc\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Pique-nique au parc\"\n Et l'écran contient le texte \"Parc Bordelais\"\n\n # --- Inscription / Désinscription ---\n\n @e2e\n Scénario: S'inscrire à un événement existant\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur le premier événement\n Et l'utilisateur clique sur le bouton \"Participer\"\n Alors l'écran contient le texte \"Inscrit\"\n\n @e2e\n Scénario: Se désinscrire d'un événement\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur le premier événement\n Et l'utilisateur clique sur le bouton \"Inscrit\"\n Alors l'écran contient le texte \"Participer\"\n\n # --- Modification ---\n\n @e2e\n Scénario: Modifier le titre d'un événement créé\n Quand l'utilisateur navigue vers l'écran \"home\"\n Et l'utilisateur clique sur l'événement \"Pique-nique au parc\"\n Et l'utilisateur clique sur le bouton de modification\n Et l'utilisateur modifie le champ \"Nom de l'événement\" avec \"Pique-nique d'été\"\n Et l'utilisateur clique sur le bouton \"Enregistrer les modifications\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Pique-nique d'été\"\n\n @e2e\n Scénario: La modification persiste après reconnexion\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran contient le texte \"Pique-nique d'été\"\n Et l'écran ne contient pas le texte \"Pique-nique au parc\"\n", + "rawContent": "# language: fr\n@EVENT @priority-1\nFonctionnalité: Cycle de vie d'un événement\n En tant qu'utilisateur connecté\n Je peux créer, consulter, modifier et participer à des événements\n Et ces actions persistent dans mon portefeuille NextGraph\n\n Contexte:\n Étant donné que le portefeuille contient des données de test\n\n # --- Création et persistance ---\n\n @e2e\n Scénario: Créer un événement et vérifier qu'il apparaît sur l'accueil\n Quand l'utilisateur navigue vers l'écran \"create-event\"\n Et l'utilisateur remplit le formulaire de création d'événement:\n | champ | valeur |\n | Nom de l'événement | Pique-nique au parc |\n | Date de début | 2026-06-15 |\n | Heure de début | 14:00 |\n | Lieu | Parc Bordelais, Bordeaux |\n Et l'utilisateur clique sur le bouton \"Relayer l'événement\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Pique-nique au parc\"\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran contient le texte \"Pique-nique au parc\"\n\n @e2e\n Scénario: L'événement créé persiste après reconnexion\n Alors l'écran d'accueil contient le texte \"Pique-nique au parc\"\n\n # --- Consultation ---\n\n @e2e\n Scénario: Consulter le détail d'un événement depuis l'accueil\n Quand l'utilisateur clique sur un événement de l'accueil\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Participants\"\n\n # --- Inscription / Désinscription ---\n\n @e2e\n Scénario: S'inscrire à un événement\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur un événement de la liste\n Et l'utilisateur attend que l'écran \"event-detail\" soit affiché\n Et l'utilisateur clique sur le bouton \"J'y serai\" si visible\n Alors l'écran contient le texte \"Je participe\"\n\n # ngSet.delete() updates UI but doesn't persist — NG ORM limitation.\n @e2e @wip\n Scénario: Se désinscrire d'un événement\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur un événement de la liste\n Et l'utilisateur attend que l'écran \"event-detail\" soit affiché\n Et l'utilisateur clique sur le bouton \"Je participe\"\n Alors l'écran contient le texte \"J'y serai\"\n\n @e2e @wip\n Scénario: La désinscription persiste après reconnexion\n Quand l'utilisateur navigue vers l'écran \"events\"\n Et l'utilisateur clique sur un événement de la liste\n Et l'utilisateur attend que l'écran \"event-detail\" soit affiché\n Alors l'écran contient le texte \"J'y serai\"\n\n # --- Modification ---\n\n @e2e\n Scénario: Modifier un événement et vérifier la persistance\n Quand l'utilisateur navigue vers l'écran \"home\"\n Et l'utilisateur clique sur un événement de l'accueil\n Et l'utilisateur attend que l'écran \"event-detail\" soit affiché\n Et l'utilisateur clique sur le bouton de modification\n Et l'utilisateur attend que l'écran \"update-event\" soit affiché\n Et l'utilisateur modifie le champ lieu avec \"Jardin Public, Bordeaux\"\n Et l'utilisateur clique sur le bouton \"Enregistrer les modifications\"\n Alors l'application affiche l'écran \"event-detail\"\n Et l'écran contient le texte \"Jardin Public\"\n", "screenIds": [] }, { @@ -748,31 +704,13 @@ export const parsedFeatures: ParsedFeature[] = [ ] }, { - "name": "Voir le bouton pour proposer un point de rencontre", + "name": "Voir le formulaire de proposition", "tags": [], "steps": [ { "keyword": "Étant donné que ", "text": "je suis sur la page \"points de rencontre\"" }, - { - "keyword": "Alors", - "text": "l'écran contient un bouton \"Proposer un point de rencontre\"" - } - ] - }, - { - "name": "Ouvrir le formulaire de proposition", - "tags": [], - "steps": [ - { - "keyword": "Étant donné que ", - "text": "je suis sur la page \"points de rencontre\"" - }, - { - "keyword": "Quand", - "text": "je clique sur \"Proposer un point de rencontre\"" - }, { "keyword": "Alors", "text": "l'écran contient un bouton \"Créer le point de rencontre\"" @@ -784,34 +722,26 @@ export const parsedFeatures: ParsedFeature[] = [ ] }, { - "name": "Définir l'heure de rencontre", + "name": "Renseigner les détails du point de rencontre", "tags": [], "steps": [ { "keyword": "Étant donné que ", "text": "je suis sur la page \"points de rencontre\"" }, - { - "keyword": "Quand", - "text": "je clique sur \"Proposer un point de rencontre\"" - }, { "keyword": "Alors", - "text": "l'écran contient un bouton \"30 min avant\"" + "text": "l'écran contient un champ \"Quand\"" }, { "keyword": "Et", - "text": "l'écran contient un bouton \"1h avant\"" - }, - { - "keyword": "Et", - "text": "l'écran contient un bouton \"Personnalisé\"" + "text": "l'écran contient un champ \"Durée\"" } ] } ], "filePath": "src/modules/meeting/features/us-16-point-rencontre.feature", - "rawContent": "# language: fr\n@MEETING @priority-1\nFonctionnalité: US-16 Indiquer un ou plusieurs points de rencontre\n En tant qu'utilisateur\n Je peux indiquer un ou plusieurs points de rencontre\n En précisant le lieu et l'heure de cette rencontre\n Afin de croiser et faire connaissance d'autres participants\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder aux points de rencontre\n Étant donné que je suis sur la page \"détail événement\"\n Quand je navigue vers \"points de rencontre\"\n Alors je vois l'écran \"meeting-points\"\n\n Scénario: Voir le bouton pour proposer un point de rencontre\n Étant donné que je suis sur la page \"points de rencontre\"\n Alors l'écran contient un bouton \"Proposer un point de rencontre\"\n\n Scénario: Ouvrir le formulaire de proposition\n Étant donné que je suis sur la page \"points de rencontre\"\n Quand je clique sur \"Proposer un point de rencontre\"\n Alors l'écran contient un bouton \"Créer le point de rencontre\"\n Et l'écran contient un champ \"Lieu\"\n\n Scénario: Définir l'heure de rencontre\n Étant donné que je suis sur la page \"points de rencontre\"\n Quand je clique sur \"Proposer un point de rencontre\"\n Alors l'écran contient un bouton \"30 min avant\"\n Et l'écran contient un bouton \"1h avant\"\n Et l'écran contient un bouton \"Personnalisé\"\n", + "rawContent": "# language: fr\n@MEETING @priority-1\nFonctionnalité: US-16 Indiquer un ou plusieurs points de rencontre\n En tant qu'utilisateur\n Je peux indiquer un ou plusieurs points de rencontre\n En précisant le lieu et l'heure de cette rencontre\n Afin de croiser et faire connaissance d'autres participants\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder aux points de rencontre\n Étant donné que je suis sur la page \"détail événement\"\n Quand je navigue vers \"points de rencontre\"\n Alors je vois l'écran \"meeting-points\"\n\n Scénario: Voir le formulaire de proposition\n Étant donné que je suis sur la page \"points de rencontre\"\n Alors l'écran contient un bouton \"Créer le point de rencontre\"\n Et l'écran contient un champ \"Lieu\"\n\n Scénario: Renseigner les détails du point de rencontre\n Étant donné que je suis sur la page \"points de rencontre\"\n Alors l'écran contient un champ \"Quand\"\n Et l'écran contient un champ \"Durée\"\n", "screenIds": [ "event-detail", "meeting-points" @@ -984,7 +914,7 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Alors", - "text": "l'URL contient \"demo/events\"" + "text": "l'URL contient \"/events\"" } ] }, @@ -999,62 +929,12 @@ export const parsedFeatures: ParsedFeature[] = [ ] }, { - "name": "Le bouton Galerie ramène à la galerie depuis le mode démo", + "name": "La liste des événements est peuplée après connexion", "tags": [], "steps": [ { "keyword": "Quand", - "text": "l'utilisateur navigue vers l'écran \"home\" sans historique" - }, - { - "keyword": "Et", - "text": "l'utilisateur clique sur le bouton \"Galerie\"" - }, - { - "keyword": "Alors", - "text": "l'application affiche la galerie" - } - ] - }, - { - "name": "Le bouton \"Charger données de test\" est visible quand connecté", - "tags": [], - "steps": [ - { - "keyword": "Alors", - "text": "la galerie affiche le bouton \"Charger données de test\"" - } - ] - }, - { - "name": "Charger les données de test remplit le portefeuille", - "tags": [], - "steps": [ - { - "keyword": "Quand", - "text": "l'utilisateur clique sur le bouton \"Charger données de test\"" - }, - { - "keyword": "Et", - "text": "l'utilisateur attend la fin du chargement" - }, - { - "keyword": "Et", - "text": "l'utilisateur navigue vers l'écran \"home\"" - }, - { - "keyword": "Alors", - "text": "l'écran d'accueil affiche des événements" - } - ] - }, - { - "name": "Les données chargées persistent après reconnexion", - "tags": [], - "steps": [ - { - "keyword": "Quand", - "text": "l'utilisateur navigue vers l'écran \"home\"" + "text": "l'utilisateur navigue vers l'écran \"events\"" }, { "keyword": "Alors", @@ -1064,7 +944,7 @@ export const parsedFeatures: ParsedFeature[] = [ } ], "filePath": "src/modules/auth/features/connexion-nextgraph.feature", - "rawContent": "# language: fr\n@AUTH @priority-1\nFonctionnalité: Connexion NextGraph et chargement des données\n En tant qu'utilisateur\n Je peux me connecter à mon portefeuille NextGraph\n Et charger les données de test dans mon portefeuille\n Afin d'utiliser l'application avec mes propres données\n\n # --- UI layer: écran de connexion ---\n\n @ui\n Scénario: L'écran de connexion affiche le bouton NextGraph\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran contient un bouton \"Se connecter avec NextGraph\"\n\n @ui\n Scénario: L'écran de connexion redirige automatiquement quand connecté\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran gère la redirection automatique après connexion\n\n @ui\n Scénario: L'état initial est \"en cours\" quand une connexion est en attente\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran gère l'état de connexion en cours\n\n @ui\n Scénario: Aucune donnée de démonstration n'est visible pendant la connexion\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran n'importe pas de données de démonstration\n\n # --- Data layer: comportement du portefeuille ---\n\n @data\n Scénario: Un portefeuille connecté est vide par défaut\n Alors le portefeuille est connecté\n Et le portefeuille ne contient aucun événement de démonstration\n\n @data\n Scénario: Charger les données de test dans le portefeuille\n Étant donné que le portefeuille est vide\n Quand je charge les données de test\n Alors le portefeuille contient des événements\n Et le portefeuille contient des utilisateurs\n\n @data\n Scénario: Les données de test ne sont pas rechargées si le portefeuille contient déjà des données\n Étant donné que le portefeuille contient déjà des événements\n Quand je charge les données de test\n Alors le nombre d'événements n'a pas changé\n\n @data\n Scénario: Les données du portefeuille sont distinctes des données par défaut\n Étant donné que le portefeuille est vide\n Quand je charge les données de test\n Alors les événements ont des identifiants NextGraph\n Et les utilisateurs ont des identifiants NextGraph\n\n # --- E2E layer: comportement réel dans le navigateur ---\n\n @e2e\n Scénario: L'écran de connexion redirige vers l'accueil si déjà connecté\n Quand l'utilisateur navigue vers l'écran \"login\"\n Alors l'application affiche l'écran \"home\"\n\n @e2e\n Scénario: La navigation interne met à jour l'URL\n Quand l'utilisateur navigue vers l'écran \"events\"\n Alors l'URL contient \"demo/events\"\n\n @e2e\n Scénario: L'application ne redirige pas vers le broker quand elle est dans l'iframe\n Alors l'application est toujours dans l'iframe\n\n @e2e\n Scénario: Le bouton Galerie ramène à la galerie depuis le mode démo\n Quand l'utilisateur navigue vers l'écran \"home\" sans historique\n Et l'utilisateur clique sur le bouton \"Galerie\"\n Alors l'application affiche la galerie\n\n @e2e\n Scénario: Le bouton \"Charger données de test\" est visible quand connecté\n Alors la galerie affiche le bouton \"Charger données de test\"\n\n @e2e\n Scénario: Charger les données de test remplit le portefeuille\n Quand l'utilisateur clique sur le bouton \"Charger données de test\"\n Et l'utilisateur attend la fin du chargement\n Et l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran d'accueil affiche des événements\n\n @e2e\n Scénario: Les données chargées persistent après reconnexion\n Quand l'utilisateur navigue vers l'écran \"home\"\n Alors l'écran d'accueil affiche des événements\n", + "rawContent": "# language: fr\n@AUTH @priority-1\nFonctionnalité: Connexion NextGraph et chargement des données\n En tant qu'utilisateur\n Je peux me connecter à mon portefeuille NextGraph\n Et charger les données de test dans mon portefeuille\n Afin d'utiliser l'application avec mes propres données\n\n # --- UI layer: écran de connexion ---\n\n @ui\n Scénario: L'écran de connexion affiche le bouton NextGraph\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran contient un bouton \"Se connecter avec NextGraph\"\n\n @ui @wip\n # Behavioral: requires simulating an NG status change. Better tested at the\n # @e2e layer where a real connected session triggers the redirect.\n Scénario: L'écran de connexion redirige automatiquement quand connecté\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran gère la redirection automatique après connexion\n\n @ui\n Scénario: L'état initial est \"en cours\" quand une connexion est en attente\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran gère l'état de connexion en cours\n\n @ui\n Scénario: Aucune donnée de démonstration n'est visible pendant la connexion\n Étant donné je suis sur la page \"connexion\"\n Alors l'écran n'importe pas de données de démonstration\n\n # --- Data layer: comportement du portefeuille ---\n\n @data\n Scénario: Un portefeuille connecté est vide par défaut\n Alors le portefeuille est connecté\n Et le portefeuille ne contient aucun événement de démonstration\n\n @data\n Scénario: Charger les données de test dans le portefeuille\n Étant donné que le portefeuille est vide\n Quand je charge les données de test\n Alors le portefeuille contient des événements\n Et le portefeuille contient des utilisateurs\n\n @data\n Scénario: Les données de test ne sont pas rechargées si le portefeuille contient déjà des données\n Étant donné que le portefeuille contient déjà des événements\n Quand je charge les données de test\n Alors le nombre d'événements n'a pas changé\n\n @data\n Scénario: Les données du portefeuille sont distinctes des données par défaut\n Étant donné que le portefeuille est vide\n Quand je charge les données de test\n Alors les événements ont des identifiants NextGraph\n Et les utilisateurs ont des identifiants NextGraph\n\n # --- E2E layer: comportement réel dans le navigateur ---\n\n @e2e\n Scénario: L'écran de connexion redirige vers l'accueil si déjà connecté\n Quand l'utilisateur navigue vers l'écran \"login\"\n Alors l'application affiche l'écran \"home\"\n\n @e2e\n Scénario: La navigation interne met à jour l'URL\n Quand l'utilisateur navigue vers l'écran \"events\"\n Alors l'URL contient \"/events\"\n\n @e2e\n Scénario: L'application ne redirige pas vers le broker quand elle est dans l'iframe\n Alors l'application est toujours dans l'iframe\n\n @e2e\n Scénario: La liste des événements est peuplée après connexion\n Quand l'utilisateur navigue vers l'écran \"events\"\n Alors l'écran d'accueil affiche des événements\n", "screenIds": [ "login" ] @@ -1402,7 +1282,7 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Et", - "text": "l'écran contient un texte \"Mes amis\"" + "text": "l'écran contient un texte \"Mon réseau\"" } ] }, @@ -1467,7 +1347,7 @@ export const parsedFeatures: ParsedFeature[] = [ } ], "filePath": "src/modules/user/features/us-20-profil-reseau.feature", - "rawContent": "# language: fr\n@USER @priority-1\nFonctionnalité: US-20 Voir le profil des personnes faisant partie de mon réseau\n En tant qu'utilisateur\n Je peux voir le profil des personnes faisant partie de mon réseau\n Ainsi que le profil des personnes publiques\n Et consulter la description de l'événement afin de savoir si je veux participer\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder à mon profil\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"mon profil\"\n Alors je vois l'écran \"profile\"\n\n Scénario: Voir mon réseau\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient un texte \"Amis\"\n Et l'écran contient un texte \"Mes amis\"\n\n Scénario: Voir un profil de mon réseau\n Étant donné que je suis sur la page \"mon profil\"\n Quand je clique sur un participant\n Alors je vois l'écran \"user-profile\"\n\n Scénario: Consulter un événement depuis un profil\n Étant donné que je suis sur la page \"profil utilisateur\"\n Quand je clique sur un événement\n Alors je vois l'écran \"event-detail\"\n\n Scénario: Vérifier les données du profil\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient un texte \"Événements\"\n Et l'écran contient un texte \"Participations\"\n\n Scénario: Voir les profils publiques\n * Scénario non implémenté\n", + "rawContent": "# language: fr\n@USER @priority-1\nFonctionnalité: US-20 Voir le profil des personnes faisant partie de mon réseau\n En tant qu'utilisateur\n Je peux voir le profil des personnes faisant partie de mon réseau\n Ainsi que le profil des personnes publiques\n Et consulter la description de l'événement afin de savoir si je veux participer\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder à mon profil\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"mon profil\"\n Alors je vois l'écran \"profile\"\n\n Scénario: Voir mon réseau\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient un texte \"Amis\"\n Et l'écran contient un texte \"Mon réseau\"\n\n Scénario: Voir un profil de mon réseau\n Étant donné que je suis sur la page \"mon profil\"\n Quand je clique sur un participant\n Alors je vois l'écran \"user-profile\"\n\n Scénario: Consulter un événement depuis un profil\n Étant donné que je suis sur la page \"profil utilisateur\"\n Quand je clique sur un événement\n Alors je vois l'écran \"event-detail\"\n\n Scénario: Vérifier les données du profil\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient un texte \"Événements\"\n Et l'écran contient un texte \"Participations\"\n\n Scénario: Voir les profils publiques\n * Scénario non implémenté\n", "screenIds": [ "event-detail", "home", @@ -1475,6 +1355,48 @@ export const parsedFeatures: ParsedFeature[] = [ "user-profile" ] }, + { + "id": "read-filter", + "name": "Filtre ReadCap (ng-eventually)", + "description": "En tant que développeur Je veux valider, contre le vrai broker, que le filtre de lecture de la lib applique les ReadCap au niveau du DOCUMENT (le repo où vit chaque item) sur le vrai set réactif de l'ORM : on ne voit un document que si on détient sa ReadCap. En mono-store (tout dans un seul repo) c'est donc tout-ou-rien sur ce document — le comportement fidèle de NextGraph.", + "tags": [ + "@WORKSHOP", + "@priority-1" + ], + "category": "WORKSHOP", + "priority": 1, + "scenarios": [ + { + "name": "On ne voit un document que si on détient sa ReadCap", + "tags": [], + "steps": [ + { + "keyword": "Étant donné", + "text": "le wallet contient des participations dans un document" + }, + { + "keyword": "Quand", + "text": "je gouverne ce document par une ReadCap accordée à un autre utilisateur" + }, + { + "keyword": "Alors", + "text": "l'utilisateur courant ne voit aucune participation de ce document" + }, + { + "keyword": "Quand", + "text": "l'utilisateur courant obtient la ReadCap du document" + }, + { + "keyword": "Alors", + "text": "il voit toutes les participations du document" + } + ] + } + ], + "filePath": "src/modules/workshop/features/read-filter.feature", + "rawContent": "# language: fr\n@WORKSHOP @priority-1\nFonctionnalité: Filtre ReadCap (ng-eventually)\n En tant que développeur\n Je veux valider, contre le vrai broker, que le filtre de lecture de la lib\n applique les ReadCap au niveau du DOCUMENT (le repo où vit chaque item) sur le\n vrai set réactif de l'ORM : on ne voit un document que si on détient sa ReadCap.\n En mono-store (tout dans un seul repo) c'est donc tout-ou-rien sur ce document\n — le comportement fidèle de NextGraph.\n\n @data\n Scénario: On ne voit un document que si on détient sa ReadCap\n Étant donné le wallet contient des participations dans un document\n Quand je gouverne ce document par une ReadCap accordée à un autre utilisateur\n Alors l'utilisateur courant ne voit aucune participation de ce document\n Quand l'utilisateur courant obtient la ReadCap du document\n Alors il voit toutes les participations du document\n", + "screenIds": [] + }, { "id": "us-19", "name": "US-19 Recevoir un récapitulatif des prochaines rencontres", @@ -1502,7 +1424,7 @@ export const parsedFeatures: ParsedFeature[] = [ }, { "keyword": "Alors", - "text": "l'écran contient une section \"Mes événements à venir\"" + "text": "l'écran contient une section \"À venir\"" } ] }, @@ -1546,7 +1468,7 @@ export const parsedFeatures: ParsedFeature[] = [ } ], "filePath": "src/modules/notification/features/us-19-recapitulatif.feature", - "rawContent": "# language: fr\n# Note: US-19 concerne les récapitulatifs par email - non testable via écrans\n# Les scénarios ci-dessous testent l'affichage sur l'écran d'accueil (aspect UI)\n@NOTIF @priority-2\nFonctionnalité: US-19 Recevoir un récapitulatif des prochaines rencontres\n En tant qu'utilisateur\n Je peux recevoir un récapitulatif des prochaines rencontres\n En réceptionnant une liste des événements auxquels je suis inscrit ou qui sont proches de chez moi\n Afin d'établir un programme des événements auxquels je participe par période\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Voir les événements à venir sur l'accueil\n Étant donné que je suis sur la page \"accueil\"\n Alors l'écran contient une section \"Mes événements à venir\"\n\n Scénario: Voir le récapitulatif par période\n * Scénario non implémenté\n\n Scénario: Voir les événements proches géographiquement\n * Scénario non implémenté\n\n Scénario: Voir mes inscriptions\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient une section \"Mes événements à venir\"\n\n Scénario: Vérifier les données de l'accueil\n Étant donné que je suis sur la page \"accueil\"\n Alors les événements affichent leur lieu\n", + "rawContent": "# language: fr\n# Note: US-19 concerne les récapitulatifs par email - non testable via écrans\n# Les scénarios ci-dessous testent l'affichage sur l'écran d'accueil (aspect UI)\n@NOTIF @priority-2\nFonctionnalité: US-19 Recevoir un récapitulatif des prochaines rencontres\n En tant qu'utilisateur\n Je peux recevoir un récapitulatif des prochaines rencontres\n En réceptionnant une liste des événements auxquels je suis inscrit ou qui sont proches de chez moi\n Afin d'établir un programme des événements auxquels je participe par période\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Voir les événements à venir sur l'accueil\n Étant donné que je suis sur la page \"accueil\"\n Alors l'écran contient une section \"À venir\"\n\n Scénario: Voir le récapitulatif par période\n * Scénario non implémenté\n\n Scénario: Voir les événements proches géographiquement\n * Scénario non implémenté\n\n Scénario: Voir mes inscriptions\n Étant donné que je suis sur la page \"mon profil\"\n Alors l'écran contient une section \"Mes événements à venir\"\n\n Scénario: Vérifier les données de l'accueil\n Étant donné que je suis sur la page \"accueil\"\n Alors les événements affichent leur lieu\n", "screenIds": [ "home", "profile" @@ -1712,7 +1634,7 @@ export const parsedFeatures: ParsedFeature[] = [ { "id": "us-26", "name": "US-26 Définir la portée d'un événement", - "description": "En tant qu'utilisateur Je peux relayer/présenter le contenu d'un événement et le catégoriser par type/thématique En indiquant son rayon d'intérêt en kilomètres Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification", + "description": "En tant qu'utilisateur Je peux relayer/présenter le contenu d'un événement En indiquant son rayon d'intérêt en kilomètres Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification", "tags": [ "@USER", "@priority-2" @@ -1749,20 +1671,6 @@ export const parsedFeatures: ParsedFeature[] = [ "tags": [], "steps": [] }, - { - "name": "Choisir une thématique", - "tags": [], - "steps": [ - { - "keyword": "Étant donné que ", - "text": "je suis sur la page \"relayer un événement\"" - }, - { - "keyword": "Alors", - "text": "l'écran contient une section \"Thématique\"" - } - ] - }, { "name": "Vérifier les champs obligatoires", "tags": [], @@ -1779,7 +1687,7 @@ export const parsedFeatures: ParsedFeature[] = [ } ], "filePath": "src/modules/user/features/us-26-portee-evenement.feature", - "rawContent": "# language: fr\n@USER @priority-2\nFonctionnalité: US-26 Définir la portée d'un événement\n En tant qu'utilisateur\n Je peux relayer/présenter le contenu d'un événement et le catégoriser par type/thématique\n En indiquant son rayon d'intérêt en kilomètres\n Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder au formulaire de relai d'événement\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"relayer un événement\"\n Alors je vois l'écran \"create-event\"\n\n Scénario: Définir le rayon d'intérêt\n * Scénario non implémenté\n\n Scénario: Choisir une thématique\n Étant donné que je suis sur la page \"relayer un événement\"\n Alors l'écran contient une section \"Thématique\"\n\n Scénario: Vérifier les champs obligatoires\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire contient les champs obligatoires suivants:\n | Nom de l'événement |\n | Date de début |\n | Heure de début |\n | Lieu |\n | Thématique |\n", + "rawContent": "# language: fr\n@USER @priority-2\nFonctionnalité: US-26 Définir la portée d'un événement\n En tant qu'utilisateur\n Je peux relayer/présenter le contenu d'un événement\n En indiquant son rayon d'intérêt en kilomètres\n Afin de m'assurer que les utilisateurs qui habitent trop loin ne reçoivent pas de notification\n\n Contexte:\n Étant donné que je suis connecté en tant qu'utilisateur\n\n Scénario: Accéder au formulaire de relai d'événement\n Étant donné que je suis sur la page \"accueil\"\n Quand je navigue vers \"relayer un événement\"\n Alors je vois l'écran \"create-event\"\n\n Scénario: Définir le rayon d'intérêt\n * Scénario non implémenté\n\n Scénario: Vérifier les champs obligatoires\n Étant donné que l'écran \"create-event\" est affiché\n Alors le formulaire contient les champs obligatoires suivants:\n | Nom de l'événement |\n | Date de début |\n", "screenIds": [ "create-event", "home" diff --git a/src/shared/test-harness/harness-ng.tsx b/src/shared/test-harness/harness-ng.tsx index 7263832..4ea6356 100644 --- a/src/shared/test-harness/harness-ng.tsx +++ b/src/shared/test-harness/harness-ng.tsx @@ -12,7 +12,7 @@ import { createRoot } from 'react-dom/client'; import { NextGraphProvider, useNextGraph } from '../context/NextGraphContext'; import { FestipodDataProvider, useFestipodData } from '../context/FestipodDataContext'; import { useShape } from '@ng-eventually/client'; -import { setGrantOf, setCurrentUser } from '@ng-eventually/client/polyfill'; +import { getCaps, setCurrentUser, resetCaps } from '@ng-eventually/client/polyfill'; import type { DeepSignalSet } from '@ng-eventually/client'; import { FpEventShapeType, @@ -68,8 +68,9 @@ function ConnectedHarness() { const participations = useShape(FpParticipationShapeType, privateNuri) as DeepSignalSet; const [bridgeReady, setBridgeReady] = useState(false); - // Read-filter validation: when set, mounts a filtered useShape. - const [filterUser, setFilterUser] = useState(null); + // Read-filter validation: once a ReadCap policy is active, mounts + // a useShape that returns the read-filtered VIEW. + const [filterActive, setFilterActive] = useState(false); useEffect(() => { // Small delay for useShape to populate @@ -150,17 +151,26 @@ function ConnectedHarness() { return bootstrapWallet(events as any, users as any, participations as any); }, + /** The document (repo NURI) all wallet entities live in (mono-store). */ + documentNuri: privateNuri, + /** - * Enable the lib's READ FILTER on the real ORM set: each participation is - * granted to its own `user`, and the current user is `user`. - * then exposes window.__readFilter with the filtered participations. + * Put the wallet document under a ReadCap policy: grant its read cap to + * `reader` only, and set the current user to `user`. The lib's read + * filter is per-DOCUMENT, so this is all-or-nothing on that document — + * the faithful NextGraph behavior in a mono-store layout. + * then exposes window.__readFilter.snapshot() over the filtered view. */ - setupReadFilter(user: string) { - setGrantOf((item: any) => - item && item.user ? { read: [item.user], write: [item.user] } : undefined, - ); + governDocument(reader: string, user: string) { + resetCaps(); + getCaps().grantRead(privateNuri!, reader); + setCurrentUser(user); + setFilterActive(true); + }, + + /** Switch the current user (does the user now hold the document's cap?). */ + setUser(user: string) { setCurrentUser(user); - setFilterUser(user); }, }; @@ -176,15 +186,16 @@ function ConnectedHarness() { return ( <>
{bridgeReady ? 'READY' : 'LOADING_SHAPES'}
- {filterUser && privateNuri && } + {filterActive && privateNuri && } ); } // ============================================================================ -// FilterProbe — subscribes participations AFTER the read filter is enabled, so -// useShape returns a filtered view. Exposes window.__readFilter for the @data -// scenario validating the read filter on the real ORM set. +// FilterProbe — subscribes participations AFTER a ReadCap policy is active, so +// useShape returns the read-filtered VIEW. Exposes window.__readFilter.snapshot() +// (evaluated lazily → reflects the CURRENT user) for the @data scenario that +// validates the per-document read filter on the real ORM set. // ============================================================================ function FilterProbe({ privateNuri }: { privateNuri: string }) { @@ -192,8 +203,9 @@ function FilterProbe({ privateNuri }: { privateNuri: string }) { useEffect(() => { (window as any).__readFilter = { ready: true, - count: set.size, - users: [...set].map(p => p.user), + // Lazy: the filtered view reads the current user at access time, so calling + // snapshot() after setUser() reflects the new cap holder without remount. + snapshot: () => ({ count: set.size, users: [...set].map(p => p.user) }), }; }, [set]); return null;