Lot V : sur une base comme la production (migrations jusqu'à 0008), 0009, 0010 et 0011 passent dans l'ordre ; 0010 ne réécrit aucune ligne et le premier démarrage n'écrit rien du tampon

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 05:44:02 +01:00
co-authored by Claude Opus 5.5
parent 9b7345346e
commit 4c9e796137
+104
View File
@@ -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::<Vec<_>>().into();
before.run(&pool).await.expect("up to 0008");
let tables = |pool: sqlx::SqlitePool| async move {
let ballots: Vec<(String, String, Option<i64>)> =
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<String>)> = 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<i64> = sqlx::query_scalar("SELECT version FROM _sqlx_migrations ORDER BY rowid")
.fetch_all(&pool)
.await
.expect("order");
assert_eq!(order, (1..=11).collect::<Vec<i64>>(), "0009, then 0010, then 0011");
assert_eq!(tables(pool.clone()).await, written, "no row rewritten");
let added: Vec<(String, String, Option<String>)> = 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))
);
}