From 4c9e7961376c8376d398144079107c331e990eaa Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 05:44:02 +0100 Subject: [PATCH] =?UTF-8?q?Lot=20V=20:=20sur=20une=20base=20comme=20la=20p?= =?UTF-8?q?roduction=20(migrations=20jusqu'=C3=A0=200008),=200009,=200010?= =?UTF-8?q?=20et=200011=20passent=20dans=20l'ordre=20;=200010=20ne=20r?= =?UTF-8?q?=C3=A9=C3=A9crit=20aucune=20ligne=20et=20le=20premier=20d=C3=A9?= =?UTF-8?q?marrage=20n'=C3=A9crit=20rien=20du=20tampon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- crates/ld-server/tests/ballots.rs | 104 ++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/crates/ld-server/tests/ballots.rs b/crates/ld-server/tests/ballots.rs index 2a93b39..4db7e2c 100644 --- a/crates/ld-server/tests/ballots.rs +++ b/crates/ld-server/tests/ballots.rs @@ -2199,3 +2199,107 @@ async fn with_the_switch_a_text_entered_by_simulated_votes_is_published() { let fr = texts.french().expect("published"); assert!(fr.contains(body_of(&doc, &e4).lines().next().expect("line")), "{fr}"); } + +/// Production runs the migrations up to 0008 (M5, `3f10a0e`), its vote on the chain off. +/// The image of now applies 0009, 0010 and 0011, in this order; 0010 rewrites no row — +/// a ballot of before stays a ballot of the chain, its lines without outcome, the votes +/// as they were — and the first start writes nothing of the buffer. +#[tokio::test] +async fn on_a_base_like_production_the_three_migrations_pass_in_order_and_0010_rewrites_nothing() { + use sqlx::sqlite::SqliteConnectOptions; + use std::str::FromStr; + let options = SqliteConnectOptions::from_str("sqlite::memory:").expect("url").foreign_keys(true); + let pool = sqlx::sqlite::SqlitePoolOptions::new().max_connections(1).connect_with(options).await.expect("pool"); + let mut before = sqlx::migrate!("./migrations"); + before.migrations = before.migrations.iter().filter(|m| m.version <= 8).cloned().collect::>().into(); + before.run(&pool).await.expect("up to 0008"); + let tables = |pool: sqlx::SqlitePool| async move { + let ballots: Vec<(String, String, Option)> = + sqlx::query_as("SELECT id, status, block FROM ballots ORDER BY id") + .fetch_all(&pool) + .await + .expect("ballots"); + let lines: Vec<(String, String, String)> = + sqlx::query_as("SELECT ballot_id, proposal_id, choice FROM ballot_lines ORDER BY ballot_id") + .fetch_all(&pool) + .await + .expect("lines"); + let votes: Vec<(String, String, String, String, Option)> = sqlx::query_as( + "SELECT proposal_id, identity_id, choice, source, ballot_id FROM votes ORDER BY proposal_id, identity_id", + ) + .fetch_all(&pool) + .await + .expect("votes"); + (ballots, lines, votes) + }; + // The rows the image of before writes: a document, a ballot of the chain confirmed + // with its line and its vote, and one pending. + for sql in [ + "INSERT INTO groups (slug, name, policy, hidden, position) VALUES ('monnaie-libre', 'Monnaie libre', 'g1-members', 0, 0)", + "INSERT INTO documents (id, group_slug, slug, title, summary, source_lang, vote_floor, created_at) + VALUES ('d1', 'monnaie-libre', 'acte', '{\"fr\":\"Acte\"}', '{}', 'fr', 50, '2026-10-21T00:00:00Z')", + "INSERT INTO units (id, doc_id, code, section, position, inertia, created_at) VALUES ('u1', 'd1', 'E4', 'fondamental', 1, '3', '2026-10-21T00:00:00Z')", + "INSERT INTO proposals (id, unit_id, kind, title, body, status, origin, revision, created_at, updated_at) + VALUES ('p1', 'u1', 'text', 'E4', 'Un texte.', 'open', 'seed', 1, '2026-10-21T00:00:00Z', '2026-10-21T00:00:00Z')", + "INSERT INTO identities (id, kind, display_name, created_at) VALUES ('dev:chiara', 'dev', 'Chiara', '2026-10-21T00:00:00Z')", + "INSERT INTO identities (id, kind, display_name, g1_member, created_at) VALUES ('idty:9001', 'g1', 'Alice', 1, '2026-10-21T00:00:00Z')", + "INSERT INTO ballots (id, doc_id, requested_by, salt, digest, remark_hash, account, amount, status, voter_id, block, + event_index, created_at, expires_at, settled_at) + VALUES ('b1', 'd1', 'dev:chiara', 's1', 'h1', '0xr1', 'g1vote', 10, 'confirmed', 'idty:9001', 100, 1, + '2026-11-01T00:00:00Z', '2026-11-02T00:00:00Z', '2026-11-01T00:01:00Z')", + "INSERT INTO ballot_lines (ballot_id, proposal_id, revision, choice) VALUES ('b1', 'p1', 1, 'for')", + "INSERT INTO votes (proposal_id, identity_id, choice, revision, cast_at, source, block, event_index, ballot_id) + VALUES ('p1', 'idty:9001', 'for', 1, '2026-11-01T00:01:00Z', 'chain', 100, 1, 'b1')", + "INSERT INTO ballots (id, doc_id, requested_by, salt, digest, remark_hash, account, amount, status, created_at, expires_at) + VALUES ('b2', 'd1', 'dev:chiara', 's2', 'h2', '0xr2', 'g1vote', 10, 'pending', '2026-11-03T00:00:00Z', '2099-01-01T00:00:00Z')", + "INSERT INTO ballot_lines (ballot_id, proposal_id, revision, choice) VALUES ('b2', 'p1', 1, 'against')", + ] { + sqlx::query(sql).execute(&pool).await.unwrap_or_else(|e| panic!("{sql}: {e}")); + } + let written = tables(pool.clone()).await; + // The image of now. + let state = AppState::new(Config::for_tests(), pool.clone()).await.expect("state"); + let mut all = sqlx::migrate!("./migrations"); + all.set_ignore_missing(true); + all.run(&pool).await.expect("0009 to 0011"); + let order: Vec = sqlx::query_scalar("SELECT version FROM _sqlx_migrations ORDER BY rowid") + .fetch_all(&pool) + .await + .expect("order"); + assert_eq!(order, (1..=11).collect::>(), "0009, then 0010, then 0011"); + assert_eq!(tables(pool.clone()).await, written, "no row rewritten"); + let added: Vec<(String, String, Option)> = sqlx::query_as( + "SELECT b.id, b.mode, l.outcome FROM ballots b JOIN ballot_lines l ON l.ballot_id = b.id ORDER BY b.id", + ) + .fetch_all(&pool) + .await + .expect("columns"); + assert_eq!(added, [("b1".into(), "chain".into(), None), ("b2".into(), "chain".into(), None)]); + let index: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = 'ballots_doc'") + .fetch_one(&pool) + .await + .expect("index"); + assert_eq!(index, 1); + // The first start, the chain closed: the watcher passes, nothing of the buffer is written. + ballots::sweep(&state).await.expect("sweep"); + assert_eq!(tables(pool.clone()).await, written); + let simulated: i64 = sqlx::query_scalar( + "SELECT (SELECT COUNT(*) FROM ballots WHERE mode = 'simulated') + (SELECT COUNT(*) FROM votes WHERE source = 'simulated') + + (SELECT COUNT(*) FROM messages WHERE topic = 'ballot')", + ) + .fetch_one(&pool) + .await + .expect("simulated"); + assert_eq!(simulated, 0); + // The ballots of before read as they did: a vote of the chain, recorded; the one + // pending, an intention that takes its place in the totals shown only. + let mine = ballots::my_votes(&state, "dev:chiara", "d1").await.expect("mine"); + assert_eq!(mine.get("p1").map(|v| (v.choice.as_str(), v.source.as_str())), Some(("for", "chain"))); + let st = ld_server::docs::load_state(&state, "d1").await.expect("state"); + let pair = |t: ld_engine::threshold::Tally| (t.for_votes, t.against); + assert_eq!( + (pair(st.tally("p1")), pair(st.shown_tally("p1")), pair(st.pending_tally("p1"))), + ((1, 0), (0, 1), (0, 1)) + ); +}