Lot V : contrôle C34 (la chaîne fermée, Monnaie libre vote quand même : le vote simulé compte aussitôt, marqué dans « Tes bulletins ») ; C24 dit que le serveur sans compte des votes simule depuis le lot V ; CLAUDE.md et CONTROLES.md (buffer, casts_simulated, simulation.rs, C34, 35 contrôles / 36 tests)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 06:03:33 +01:00
co-authored by Claude Opus 5.5
parent f62347cc6b
commit c06c37a24a
3 changed files with 61 additions and 7 deletions
+10 -5
View File
@@ -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
@@ -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-<date>-<id>.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
+50 -2
View File
@@ -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);
}
});