From c7d2791ad17cde4923ed01e2ee4f573f9bb29629 Mon Sep 17 00:00:00 2001 From: Yvv Date: Sat, 31 Oct 2026 03:43:17 +0100 Subject: [PATCH] =?UTF-8?q?v3=20:=20C32=20et=20C33=20votent=20=C2=AB=20con?= =?UTF-8?q?tre=20=C2=BB=20sur=20des=20propositions=20qu'aucun=20autre=20co?= =?UTF-8?q?ntr=C3=B4le=20ne=20lit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le texte de Monnaie libre reste tel que C20 et C23 l'attendent (ils passent après dans l'ordre des fichiers) ; rien n'entre en vigueur, rien ne devient bien coté. Co-Authored-By: Claude Opus 5.5 --- .../chantiers/acte-certification/CONTROLES.md | 4 +-- web/e2e/bulletins.spec.ts | 35 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/chantiers/acte-certification/CONTROLES.md b/docs/chantiers/acte-certification/CONTROLES.md index f599d1a..2fb1de0 100644 --- a/docs/chantiers/acte-certification/CONTROLES.md +++ b/docs/chantiers/acte-certification/CONTROLES.md @@ -37,8 +37,8 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv. | C22 | Le vote groupé | groupe d'essai : « Propositions en cours » → « Engagements techniques » → « Voter cette section » ; « Tout pour » ; dans « Connaissance suffisante », « Pour » sur la variante 1.0.0 ; recharger ; « Voter (n) » | le panier s'ouvre sur la section ; « Tout pour » remplit les articles à une version, pas « Connaissance suffisante » ni « Rencontre physique… » (« Plusieurs versions : choisis la tienne. », et le compte des articles laissés) ; le panier reste après rechargement (« Voter en lot · n ») ; un seul envoi : « n votes enregistrés. », le panier vide, la variante choisie à 1 pour | | C23 | Le vote par la chaîne Ğ1 | Monnaie libre, un portefeuille dans le navigateur : « Voter en lot », « Pour » sur Unicité du compte et Réciprocité, « Signer et envoyer (2) » ; puis sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : « Pour » → « Passer par Ğecko » | une seule signature pour les deux : 0,1 Ğ1 vers le compte des votes et une remarque `ld1:` + 64 chiffres, rien du vote en clair ; « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « Compté pour l'identité Ğ1 « Alice ». » ; sous l'article « Ton vote : pour · inscrit sur la chaîne Ğ1 », 1 pour ; « En attente de la chaîne Ğ1 : contre. » ; le retrait, plus récent sur la chaîne, fait foi même quand le « contre » est confirmé après lui (0 pour · 0 contre) ; Ğecko : un QR code et le lien `g1://?amount=0.1&comment=ld1:…`, puis la confirmation dès que la chaîne la montre | | C24 | Sans compte des votes | Monnaie libre sur un serveur sans `LD_VOTE_ACCOUNT`, puis le groupe d'essai | pas de « Voter en lot » ni « Voter cette section » ; sous l'article : « Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer. Tu peux lire, proposer et signaler une contradiction. », pas de bouton, les totaux et le seuil restent ; le panier ouvert par son adresse (`?onglet=panier`) dit la même chose, son bouton éteint, et **jamais** « Tes votes comptent dès l'envoi » ; dans le groupe d'essai, le panier et les boutons sont là | -| C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée) : Réciprocité → « 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 : « Pour » → « 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 : pour. », puis confirmé sans la page | -| C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Pour » sur Unicité du compte, « 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. », « Unicité du compte : pour » ; 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é | +| 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é | ## Au-delà de la liste diff --git a/web/e2e/bulletins.spec.ts b/web/e2e/bulletins.spec.ts index 89e7574..aef3235 100644 --- a/web/e2e/bulletins.spec.ts +++ b/web/e2e/bulletins.spec.ts @@ -5,18 +5,23 @@ import { ALICE, chain, DOC, EVE, person, shot, signIn, VOTE_ACCOUNT, wallet } fr // Lot B2 — the vote on the Ğ1 chain, after its review: a ballot nothing left for // is let go; signed, or shown as a Ğecko code, it is kept and the server confirms // it without the page; the receipt goes to the one who asked, who checks it here. -// The chain is simulated (e2e/chain-mock.mjs): never a real transaction. +// The chain is simulated (e2e/chain-mock.mjs): never a real transaction. The votes go +// to open proposals no other control reads, and only « contre »: nothing enters into +// force, nothing becomes well rated, the other controls find the text as they expect. type Entry = { id: string; status: string; comment: string }; const mine = async (page: Page): Promise => (await (await page.request.get('/api/ballots')).json()).ballots; +/** Unfolds the vote of an open proposal, from « Propositions en cours ». */ async function unfold(page: Page, title: string) { - const unit = page.locator('section.text article.unit', { hasText: title }); - await unit.hover(); - await unit.getByRole('button', { name: 'Le vote' }).click(); + // By its title only: the excerpt of another proposal may name it. + await page.locator('ul.deep button.row').filter({ has: page.locator('span.title').getByText(title, { exact: true }) }).click(); return page.getByRole('region', { name: `Le vote de « ${title} »` }); } +const REVOCATION = 'Document de révocation'; +const NEVER_ALONE = 'Ne jamais certifier seul'; + const answer = (page: Page, yes: boolean) => page.evaluate((y) => (window as unknown as { __ldAnswer: (yes: boolean) => void }).__ldAnswer(y), yes); @@ -34,7 +39,7 @@ test('C32 — rien n’est parti : le bulletin est lâché ; signé ou par Ğeck } }); const deleted = () => asked.filter((a) => a.startsWith('DELETE')).length; - const fold = await unfold(page, 'Réciprocité'); + const fold = await unfold(page, REVOCATION); const dialog = page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' }); // The node does not answer before anything is signed: nothing left, the ballot is let go. @@ -69,18 +74,18 @@ test('C32 — rien n’est parti : le bulletin est lâché ; signé ou par Ğeck await expect.poll(async () => (await mine(page)).map((b) => b.status), { timeout: 30_000 }).toEqual(['confirmed']); expect(deleted()).toBe(2); await page.goto(DOC); - await expect((await unfold(page, 'Réciprocité')).locator('.voted')).toHaveText('Ton vote : contre · inscrit sur la chaîne Ğ1'); + await expect((await unfold(page, REVOCATION)).locator('.voted')).toHaveText('Ton vote : contre · inscrit sur la chaîne Ğ1'); // Ğecko: the code shown, the window closed — the ballot is kept; paid from a phone, confirmed by the server. const ugo = await person(browser, 'Ugo'); await ugo.goto(DOC); - const theirs = await unfold(ugo, 'Réciprocité'); - await theirs.getByRole('button', { name: 'Pour', exact: true }).click(); + const theirs = await unfold(ugo, REVOCATION); + await theirs.getByRole('button', { name: 'Contre', exact: true }).click(); const sheet = ugo.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' }); await sheet.getByRole('button', { name: 'Passer par Ğecko' }).click(); const link = (await sheet.getByRole('link', { name: 'Ouvrir dans Ğecko' }).getAttribute('href')) ?? ''; await sheet.getByRole('button', { name: 'Fermer' }).click(); - await expect(theirs.getByText('En attente de la chaîne Ğ1 : pour.')).toBeVisible(); + await expect(theirs.getByText('En attente de la chaîne Ğ1 : contre.')).toBeVisible(); const { node } = await chain(ugo); const fields = { address: ALICE.address, dest: VOTE_ACCOUNT, amount: '10', remark: link.split('comment=')[1] ?? '', signature: '0x01' }; await ugo.request.post(node, { @@ -88,7 +93,7 @@ test('C32 — rien n’est parti : le bulletin est lâché ; signé ou par Ğeck }); await expect.poll(async () => (await mine(ugo)).map((b) => b.status), { timeout: 30_000 }).toEqual(['confirmed']); await ugo.goto(DOC); - await expect((await unfold(ugo, 'Réciprocité')).locator('.voted')).toHaveText('Ton vote : pour · inscrit sur la chaîne Ğ1'); + await expect((await unfold(ugo, REVOCATION)).locator('.voted')).toHaveText('Ton vote : contre · inscrit sur la chaîne Ğ1'); }); test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins, vérifié ici ; jamais chez les autres', async ({ page, browser }) => { @@ -96,8 +101,8 @@ test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins await signIn(page, 'Ines'); await wallet(page, [ALICE]); await page.goto(DOC); - const fold = await unfold(page, 'Unicité du compte'); - await fold.getByRole('button', { name: 'Pour', exact: true }).click(); + const fold = await unfold(page, NEVER_ALONE); + await fold.getByRole('button', { name: 'Contre', exact: true }).click(); const dialog = page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' }); await expect(dialog.getByText('Ton vote compte : il est inscrit sur la chaîne Ğ1.')).toBeVisible({ timeout: 20_000 }); const [download] = await Promise.all([page.waitForEvent('download'), dialog.getByRole('link', { name: 'Télécharger ton reçu' }).click()]); @@ -107,7 +112,7 @@ test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins expect(receipt).toMatchObject({ format: 'ld1', status: 'confirmed', voter: 'Alice', author: ALICE.address }); expect(receipt.comment).toMatch(/^ld1:[0-9a-f]{64}$/); expect(receipt.salt).toMatch(/^[0-9a-f]{32}$/); - expect(receipt.lines).toEqual([expect.objectContaining({ choice: 'for', title: 'Unicité du compte' })]); + expect(receipt.lines).toEqual([expect.objectContaining({ choice: 'against', title: NEVER_ALONE })]); // The same ballot, in her ballots. await dialog.getByRole('link', { name: 'Tes bulletins' }).click(); @@ -121,9 +126,9 @@ test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins // Checked here, in the browser, without the server. await page.getByLabel('Fichier du reçu').setInputFiles(file); await expect(page.getByText('Le reçu tient : ses votes et son sel donnent bien ce commentaire.')).toBeVisible(); - await expect(page.getByText('Unicité du compte : pour')).toBeVisible(); + await expect(page.getByText(`${NEVER_ALONE} : contre`)).toBeVisible(); // One choice changed: it no longer holds. - const changed = { ...receipt, lines: receipt.lines.map((l: object) => ({ ...l, choice: 'against' })) }; + const changed = { ...receipt, lines: receipt.lines.map((l: object) => ({ ...l, choice: 'for' })) }; await page.getByLabel('Ou colle le reçu').fill(JSON.stringify(changed)); await page.getByRole('button', { name: 'Vérifier', exact: true }).click(); await expect(page.getByText('Le reçu ne tient pas : ses votes et son sel ne donnent pas son commentaire.')).toBeVisible();