diff --git a/CLAUDE.md b/CLAUDE.md index cdc0a8d..388636c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,10 +40,13 @@ crates/ # fait foi, jugement d'une transaction), address (SS58, préfixe g1), objection # (redit / contredit : la dernière arrivée retenue, les tours, l'échéance), correction # (la coquille d'un autre : attendre, valider, refuser, l'échéance), order (l'ordre - # d'une section : qui en est, complété à la fin, places), pending (modules à voter) + # d'une section : qui en est, complété à la fin, places), pending (modules à voter), + # buffer (le tampon des votes, ADR 0012 : réconciliation, poids des états, + # SIMULATED_ADOPTS) trustin-access/ # règles d'accès de trustinWallet : sees / opens / manages_access, vote_way (serveur ou # chaîne), answers_typo, lifts_objection / reiterates_objection, reads_message, - # sets_quorum, groupes par défaut (TEST_GROUP) + # sets_quorum, casts_simulated (le vote simulé, chaîne fermée), groupes par défaut + # (TEST_GROUP) ld-server/ migrations/ # schéma SQLite — additif seulement src/ # lib.rs (routes, csrf, run) · config.rs (LD_*) · auth.rs (connect OAuth2, invitations, @@ -55,7 +58,9 @@ crates/ # jour) · publish.rs (file → git) · wot.rs (W sur la chaîne) · seed.rs · # seed_amend.rs / inertia_amend.rs / seed_proposal_amend.rs (amendements gardés ; # le dernier fait des Propositions seed, ADR 0011) · ballots.rs (vote groupé, - # bulletins de la chaîne Ğ1, confirmation, « ton vote ») · indexer.rs (txComments) + # bulletins de la chaîne Ğ1, le tampon : intention, preuve, réconciliation ; le vote + # simulé, « ton vote ») · simulation.rs (l'acte « supprime » de la fin de la simulation : + # `ld-server discard-simulation [--apply]`, ADR 0012) · indexer.rs (txComments) # · modules.rs / order.rs / doc_title.rs (les modules, ADR 0009 : l'ordre d'une # section, le titre du document) · module_amend.rs (ajout gardé) · quorum.rs (le quorum # d'un article, PUT /units/{id}/quorum ; « un vote existe » : voted_units, ADR 0013) · @@ -87,7 +92,7 @@ web/src/ # · g/[group]/decisions · g/[group]/mandats · # g/[group]/protocoles (à venir) · acces (trustinWallet) · messages (la boîte) ; # /groupes, /decisions… redirigent -web/e2e/ # Playwright C1–C33 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et +web/e2e/ # Playwright C1–C34 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et # chain-mock.mjs (nœud et indexeur Ğ1 simulés) ; faux portefeuille dans helpers.ts docker/ # app.Dockerfile · app-compose.yml (sonic) .woodpecker/ # app.yml · maquette.yml @@ -109,7 +114,7 @@ maquette/ # v2 intacte (frontend Nuxt, backend FastAPI, docker, docs-v ```bash export PATH=$HOME/.cargo/bin:$PATH cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace -cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 34 contrôles (35 tests, C25 en deux) +cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 35 contrôles (36 tests, C25 en deux) LD_DEV_LOGIN=1 cargo run -p ld-server # :8002, sert web/build cd web && npm run dev # :3002 à chaud diff --git a/docs/chantiers/acte-certification/CONTROLES.md b/docs/chantiers/acte-certification/CONTROLES.md index b9f662d..df81623 100644 --- a/docs/chantiers/acte-certification/CONTROLES.md +++ b/docs/chantiers/acte-certification/CONTROLES.md @@ -49,6 +49,7 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv. | C31 | Coquille sur un texte en vigueur | groupe d'essai : une administratrice (Ada) ouvre le groupe ; Vera, sur « Raison d'être » → « Le vote » → « Coquilles, traductions, objections, historique » → « Corriger une coquille », « C'est le seul » → « C’est le seul » ; Ada ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend un administrateur, ce texte venant du seed… » ; le texte en vigueur ne change pas (« C'est le seul ») ; la correction en attente se lit dans « Détails » ; chez Ada : « Vera propose une correction de coquille dans « Raison d'être », un texte du seed : un administrateur la valide. » ; après « Valider », le texte en vigueur porte « C’est le seul » et l'historique « validée par Ada » | | C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée), « Propositions en cours » → Document de révocation : « Contre » le nœud éteint ; puis « Contre », fermer la fenêtre pendant que le portefeuille demande, refuser dans le portefeuille ; puis « Contre », fermer, signer dans le portefeuille ; une autre personne, sans portefeuille : « Contre » → « Passer par Ğecko », fermer, payer depuis un téléphone | nœud éteint : « La chaîne Ğ1 ne répond pas. Réessaie dans un moment. », le bulletin lâché aussitôt, rien « en attente » sous l'article ; fenêtre fermée puis refus : lâché ; fenêtre fermée puis signature : gardé, et la chaîne le confirme sans la page (« Ton vote : contre · inscrit sur la chaîne Ğ1 ») ; Ğecko, fenêtre fermée : « En attente de la chaîne Ğ1 : contre. », puis confirmé sans la page | | C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Contre » sur Ne jamais certifier seul (« Propositions en cours »), « Télécharger ton reçu », « Tes bulletins » ; sur « Tes bulletins », « Fichier du reçu » : le reçu ; puis coller le reçu avec un choix changé, « Vérifier » ; une autre personne ouvre « Tes bulletins » | le reçu `recu-vote--.json` : les votes, le sel, la remarque `ld1:…`, « confirmed », l'identité « Alice » ; « Tes bulletins de vote » : « Compté pour l'identité Ğ1 « Alice » » et la remarque ; « Le reçu tient : ses votes et son sel donnent bien ce commentaire. », « Ne jamais certifier seul : contre » ; changé : « Le reçu ne tient pas » ; l'autre personne : « Tu n'as encore envoyé aucun bulletin par la chaîne Ğ1. », et le reçu lui est refusé | +| C34 | Le vote simulé, la chaîne fermée (ADR 0012) | Monnaie libre sur un serveur sans compte des votes (l'automate ferme la chaîne du serveur jetable, puis la rouvre) : « Propositions en cours » → Logiciel auditable → « Contre », sans portefeuille ; puis « Tes bulletins » | « Voter en lot » est là ; aucune fenêtre de portefeuille ; « Ton vote : contre » aussitôt, et un « contre » de plus dans les totaux ; dans l'API, `myVoteState` « simulated » et `tallySimulated` ; dans « Tes bulletins » : « Simulé : compté pour « Sacha » jusqu'à l'ouverture de la chaîne Ğ1 », sans bloc | ## Au-delà de la liste diff --git a/web/e2e/vote.spec.ts b/web/e2e/vote.spec.ts index 5b66089..b6de14a 100644 --- a/web/e2e/vote.spec.ts +++ b/web/e2e/vote.spec.ts @@ -176,8 +176,9 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur test('C24 — sans compte de vote, le vote par la chaîne est fermé et le dit ; le groupe d’essai vote', async ({ page }) => { await signIn(page, 'Chiara'); - // The e2e server has its vote account: the page is shown what a server without one answers - // (the server itself: `without_a_vote_account_the_chain_vote_is_off_and_the_test_group_still_votes`). + // The e2e server has its vote account: the page is shown a vote closed, as a server without + // one answered before lot V. Since lot V (ADR 0012) a server without one votes in simulation + // instead (the server: `without_a_vote_account_monnaie_libre_votes_in_simulation_…`; here, C34). await page.route(/\/api\/docs\/[^/?]+$/, async (route) => { const res = await route.fetch(); const body = await res.json(); @@ -213,3 +214,50 @@ test('C24 — sans compte de vote, le vote par la chaîne est fermé et le dit ; await unit.getByRole('button', { name: 'Le vote' }).click(); await expect(unit.locator('section.fold').getByRole('button', { name: 'Pour', exact: true })).toBeVisible(); }); + +/** The version of an article of Monnaie libre, by its title, as the API tells it to `page`. */ +async function version(page: import('@playwright/test').Page, title: string) { + const g = await (await page.request.get('/api/groups/monnaie-libre')).json(); + const d = await (await page.request.get(`/api/docs/${g.docs[0].id}`)).json(); + type P = { title: string; tally: { against: number }; tallySimulated: { against: number }; myVoteState: string | null }; + const all: P[] = d.units.flatMap((u: { proposals: P[] }) => u.proposals); + const p = all.find((x) => x.title === title); + if (!p) throw new Error(`no version « ${title} »`); + return { vote: d.vote as { way: string; open: boolean; simulated: boolean }, p }; +} + +test('C34 — la chaîne fermée, Monnaie libre vote quand même : le vote simulé compte aussitôt, marqué « simulé » dans tes bulletins', async ({ page }) => { + // Lot V (ADR 0012, Yvv 2026-11-02: « Il compte, puis s'efface »). The e2e server has its + // chain: it is closed for this control only, then opened again. The vote goes to a + // proposal no other control reads, and only « contre »: nothing enters into force. + await signIn(page, 'Sacha'); + const chainOpen = async (open: boolean) => + (await (await page.request.post('/api/dev/vote-chain', { data: { open }, headers: { 'x-requested-with': 'libredecision' } })).json()).open; + const title = 'Logiciel auditable'; + expect(await chainOpen(false)).toBe(false); + try { + const before = await version(page, title); + expect(before.vote).toMatchObject({ way: 'server', open: true, simulated: true }); + await page.goto(DOC); + // The basket is there, without a wallet: the vote leaves by the server. + await expect(page.getByRole('button', { name: /^Voter en lot/ })).toBeVisible(); + await deepRow(page, title).click(); + const fold = page.getByRole('region', { name: `Le vote de « ${title} »` }); + await fold.getByRole('button', { name: 'Contre', exact: true }).click(); + await expect(page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' })).toHaveCount(0); + await expect(fold.locator('.voted')).toHaveText(/^Ton vote : contre/); + await expect(fold.getByText(`0 pour · ${before.p.tally.against + 1} contre`)).toBeVisible(); + // Counted at once, marked as simulated. + const after = await version(page, title); + expect(after.p.myVoteState).toBe('simulated'); + expect(after.p.tallySimulated.against).toBe(before.p.tallySimulated.against + 1); + // In her ballots: simulated, counted for her session, never a block of the chain. + await page.goto('/bulletins'); + const ballot = page.locator('li.ballot').first(); + await expect(ballot.locator('.state')).toHaveText("Simulé : compté pour « Sacha » jusqu'à l'ouverture de la chaîne Ğ1"); + await expect(ballot.getByText('bloc', { exact: false })).toHaveCount(0); + } finally { + // The next controls need the chain. + expect(await chainOpen(true)).toBe(true); + } +});