Fusion de chantier/acte-certification (lots H et S) dans le lot V : le tampon et ses états, avec le quorum et les Propositions seed

- conflits résolus des deux côtés : kept_params garde le quorum (S) et la part simulée, la version déplacée, le cran d'avant (V) ; proposals.rs garde seedMinimal / seedProposal (S) et tallyPending / tallySimulated / myVoteState (V) ;
- simulation::reexamine juge sur le quorum gardé par l'adoption (params.quorum, à défaut celui de l'article) ;
- l'acte répété n'examine plus une adoption qui tient (params.reexamined) ;
- tests V repris sans texte en vigueur au départ (I1, E1 : la Proposition seed) et sur un quorum 2 (small_web) ; le test overview d'une personne invitée dans Monnaie libre suit la simulation (elle vote, chaîne fermée).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 05:32:51 +01:00
co-authored by Claude Opus 5.5
66 changed files with 5134 additions and 701 deletions
+147 -91
View File
@@ -268,6 +268,39 @@ impl Client {
}
}
/// The quorum of an article set to 2, as its author — an administrator, for a text of the
/// seed — sets it before the first vote (ADR 0013). In the test group, where the quorum
/// of every level is 1 and one voice passes, one voice of two then no longer does.
async fn two_voices(state: &AppState, group: &str, code: &str) {
let n = sqlx::query(
"UPDATE units SET quorum = 2 WHERE code = ? AND doc_id IN (SELECT id FROM documents WHERE group_slug = ?)",
)
.bind(code)
.bind(group)
.execute(&state.db)
.await
.expect("quorum")
.rows_affected();
assert_eq!(n, 1, "{group} {code}");
}
/// A web of two members and the quorum 2 at every level of a document, for the test
/// only (ADR 0013): two voices pass, one never does.
async fn small_web(state: &AppState, doc_id: &str) {
sqlx::query("UPDATE documents SET vote_floor = 1, quorums = '[2,2,2,2,2]' WHERE id = ?")
.bind(doc_id)
.execute(&state.db)
.await
.expect("quorums");
sqlx::query(
"INSERT INTO settings (key, value, updated_at) VALUES ('wot_size', '2', '2026-11-02T00:00:00Z')
ON CONFLICT(key) DO UPDATE SET value = '2'",
)
.execute(&state.db)
.await
.expect("web");
}
fn unit<'a>(doc: &'a Value, code: &str) -> &'a Value {
doc["units"].as_array().expect("units").iter().find(|u| u["code"] == code).expect("unit")
}
@@ -351,7 +384,7 @@ async fn a_grouped_ballot_is_sealed_then_confirmed_by_the_indexer() {
assert_eq!(doc["vote"]["chain"]["amount"], 10);
assert_eq!(doc["vote"]["chain"]["test"], false);
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let e3 = version(&doc, "E3", "0");
let b = c.ballot(&doc, &[(&i1, Some("for")), (&e3, Some("against"))], Some(ALICE_GENERIC)).await;
let comment = b["comment"].as_str().expect("comment");
@@ -411,8 +444,11 @@ async fn a_grouped_ballot_is_sealed_then_confirmed_by_the_indexer() {
.await
.expect("votes");
assert!(voters.iter().all(|(who, source)| who == "idty:9001" && source == "chain"), "{voters:?}");
// One voice never makes a text pass (C = 50).
assert_eq!(find(&doc, &i1)["evaluation"]["required"], 50);
// One voice never makes a text pass: at level 2, the quorum 12, the 11 voters missing
// counted as against — 9 « pour » are asked (ADR 0013).
let e = &find(&doc, &i1)["evaluation"];
assert_eq!((e["quorum"].as_u64(), e["required"].as_u64(), e["yesNeeded"].as_u64()), (Some(12), Some(9), Some(9)));
assert_eq!(e["reached"], false);
// Someone else sees the totals, not the vote.
let mut yvv = c.other();
yvv.login("Yvv").await;
@@ -442,7 +478,9 @@ async fn one_voice_on_the_chain_never_adopts_in_monnaie_libre() {
assert_eq!(s["status"], "confirmed");
assert_eq!(s["entered"], json!([]));
let d = c.ok("GET", &format!("/api/proposals/{p}"), None).await;
assert_eq!(d["proposal"]["evaluation"]["required"], 50);
// Level 3, the quorum 50: 39 « pour » of 50 voters, the 49 missing counted as against.
assert_eq!(d["proposal"]["evaluation"]["quorum"], 50);
assert_eq!(d["proposal"]["evaluation"]["required"], 39);
assert_eq!(d["proposal"]["evaluation"]["wellRated"], true);
assert_eq!(d["proposal"]["myVote"], "for");
assert_eq!(d["proposal"]["myVoteChain"], true);
@@ -454,7 +492,7 @@ async fn a_silent_indexer_keeps_the_ballot_pending() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
indexer.pay(100, b["comment"].as_str().expect("comment"), alice());
indexer.silent(true);
@@ -480,7 +518,7 @@ async fn a_signer_who_is_not_a_member_is_refused_and_told() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let former = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
indexer.pay(
100,
@@ -523,7 +561,7 @@ async fn a_wrong_amount_or_recipient_is_refused() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
// Bound to the wallet that signs: its own transaction without the transfer is refused, and told.
let short = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
indexer.add(100, 1, short["comment"].as_str().expect("c"), alice(), &[(VOTE_ACCOUNT, 9)]);
@@ -552,7 +590,7 @@ async fn confirming_twice_changes_nothing() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
indexer.pay(100, b["comment"].as_str().expect("c"), alice());
// The page and the background task at once.
@@ -579,8 +617,8 @@ async fn the_last_one_stands_in_the_order_of_the_chain() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let e1 = version(&doc, "E1", "inForce");
let i1 = version(&doc, "I1", "0");
let e1 = version(&doc, "E1", "0");
// Three ballots of the same person, signed in this order on the chain: for (I1, E1), against (I1), none (E1).
let first = c.ballot(&doc, &[(&i1, Some("for")), (&e1, Some("for"))], None).await;
let second = c.ballot(&doc, &[(&i1, Some("against"))], None).await;
@@ -661,7 +699,7 @@ async fn a_bound_ballot_ignores_a_copy_of_its_remark_by_another_account() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
let comment = b["comment"].as_str().expect("c");
// Charlie copies the remark seen in the pool and gets it in first.
@@ -683,7 +721,7 @@ async fn a_ballot_not_seen_in_time_is_let_go() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
let expires = b["expiresAt"].as_str().expect("expires");
assert!(
@@ -712,7 +750,7 @@ async fn a_ballot_the_wallet_refused_to_sign_is_let_go_at_once() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let first = c.ballot(&doc, &[(&i1, Some("against"))], Some(ALICE)).await;
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["id"], b["id"]);
@@ -745,7 +783,7 @@ async fn a_session_sees_its_own_ballots_only() {
let mut other = chiara.other();
other.login("Yvv").await;
let doc = chiara.doc("monnaie-libre").await;
let (i1, e1) = (version(&doc, "I1", "inForce"), version(&doc, "E1", "inForce"));
let (i1, e1) = (version(&doc, "I1", "0"), version(&doc, "E1", "0"));
// Both sign with the same wallet (one person, two sessions): two ballots, two versions.
let a = chiara.ballot(&doc, &[(&i1, Some("for"))], None).await;
let b = other.ballot(&doc, &[(&e1, Some("against"))], None).await;
@@ -763,7 +801,7 @@ async fn a_session_sees_its_own_ballots_only() {
}
#[tokio::test]
async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it() {
async fn thirty_nine_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it() {
let indexer = Indexer::start().await;
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
@@ -771,20 +809,25 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
let e4 = version(&doc, "E4", "0");
let e5 = version(&doc, "E5", "0");
let reason = "Ces deux textes disent le contraire l'un de l'autre.";
let e1 = version(&doc, "E1", "inForce");
let keys: Vec<String> = (1..=50u8).map(|n| address::encode(G1_PREFIX, &[n; 32])).collect();
let e1 = version(&doc, "E1", "0");
// Level 3, the quorum 50 (ADR 0013): 39 « pour » with no « contre ».
let needed = find(&doc, &e4)["evaluation"]["yesNeeded"].as_u64().expect("yesNeeded");
assert_eq!(needed, 39);
let needed = u8::try_from(needed).expect("n");
let keys: Vec<String> = (1..=needed).map(|n| address::encode(G1_PREFIX, &[n; 32])).collect();
let mut last = Value::Null;
let mut luna = c.other();
luna.login("Luna").await;
for (n, account) in keys.iter().enumerate() {
// The fiftieth voice comes from another session: its intention is seen at once.
let asker = if n == 49 { &mut luna } else { &mut c };
// The last voice comes from another session: its intention is seen at once.
let last_one = n + 1 == keys.len();
let asker = if last_one { &mut luna } else { &mut c };
let b = asker.ballot(&doc, &[(&e4, Some("for")), (&e5, Some("for"))], None).await;
if n == 49 {
// Fifty voices shown, forty-nine proven: nothing enters before the proof.
if last_one {
// Thirty-nine voices shown, thirty-eight proven: nothing enters before the proof.
let d = c.doc("monnaie-libre").await;
assert_eq!((tally(&d, &e4), pending(&d, &e4)), ((50, 0), (1, 0)));
assert_eq!(counted(&state, &d, &e4).await, (49, 0));
assert_eq!((tally(&d, &e4), pending(&d, &e4)), ((39, 0), (1, 0)));
assert_eq!(counted(&state, &d, &e4).await, (38, 0));
let id = d["doc"]["id"].as_str().expect("id");
assert!(ld_server::adoption::run(&state, id).await.expect("run").is_empty());
assert_ne!(unit(&c.doc("monnaie-libre").await, "E4")["inForce"]["id"], e4.as_str());
@@ -795,9 +838,9 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
b["comment"].as_str().expect("c"),
Signer { account, identity: Some((index, "Membre", true)) },
);
if n == 49 {
if n + 1 == keys.len() {
// The last ballot is signed; before the chain confirms it, an objection holds
// E5 back against the text in force E1 (ADR 0010): no new ballot may hold it.
// E5 back against the seed proposal E1, written before it (ADR 0010, 0011): no new ballot may hold it.
let o = c
.ok(
"POST",
@@ -821,11 +864,12 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
// A session holds at most 20 pending ballots: the background task confirms on the way.
ballots::sweep(&state).await.expect("sweep");
}
if n == 48 {
if n + 2 == keys.len() {
ballots::sweep(&state).await.expect("sweep");
let d = c.doc("monnaie-libre").await;
assert_eq!(tally(&d, &e4), (49, 0));
assert!(find(&d, &e4)["enteredAt"].is_null(), "49 voices: below C = 50");
assert_eq!(tally(&d, &e4), (38, 0));
assert!(find(&d, &e4)["enteredAt"].is_null(), "38 voices: one short of the 39 the quorum 50 asks");
assert_eq!(find(&d, &e4)["evaluation"]["missing"], 1);
}
}
ballots::sweep(&state).await.expect("sweep");
@@ -836,7 +880,7 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
assert_eq!(unit(&d, "E4")["inForce"]["id"], e4.as_str());
// Sealed before the objection, proven after it: its *for* on E5 is set aside
// (ADR 0012, revising 0010) — its *for* on E4 counts. Luna is told.
assert_eq!(tally(&d, &e5), (49, 0));
assert_eq!(tally(&d, &e5), (38, 0));
assert_eq!(find(&d, &e5)["votable"], false);
assert!(unit(&d, "E5")["inForce"].is_null(), "an objection holds it back");
let outcomes: Vec<(String, Option<String>)> =
@@ -863,7 +907,7 @@ async fn on_the_chain_a_held_version_takes_against_and_withdrawal_never_for() {
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let (e1, e4, e5) = (version(&doc, "E1", "inForce"), version(&doc, "E4", "0"), version(&doc, "E5", "0"));
let (e1, e4, e5) = (version(&doc, "E1", "0"), version(&doc, "E4", "0"), version(&doc, "E5", "0"));
let o = c
.ok(
"POST",
@@ -934,7 +978,7 @@ async fn a_long_author_is_refused_at_once() {
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
// The body may hold 256 KiB: an author of 250 000 characters passes it.
for n in [80_000usize, 250_000] {
let body = json!({ "lines": [{ "proposalId": i1, "choice": "for" }], "author": "z".repeat(n) });
@@ -953,7 +997,7 @@ async fn the_background_task_reaches_every_pending_ballot() {
let (mut a, state) = Client::new(chain_config(&indexer)).await;
a.login("Anna").await;
let doc = a.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let mut b = a.other();
b.login("Bruno").await;
let mut c = a.other();
@@ -988,7 +1032,7 @@ async fn letting_ballots_go_does_not_lift_the_cap() {
a.login("Anna").await;
let doc = a.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
for _ in 0..20 {
let b = a.ballot(&doc, &[(&i1, Some("for"))], None).await;
a.ok("DELETE", &format!("/api/ballots/{}", id_of(&b)), None).await;
@@ -1043,7 +1087,7 @@ async fn an_unbound_ballot_is_not_settled_by_an_unpaid_copy() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
indexer.add(99, 1, comment_of(&b), Signer { account: OTHER, identity: None }, &[]);
indexer.add(
@@ -1070,7 +1114,7 @@ async fn an_unbound_ballot_can_be_taken_by_a_paid_copy() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
indexer.pay(99, comment_of(&b), Signer { account: CHARLIE, identity: Some((7, "Charlie", true)) });
indexer.pay(100, comment_of(&b), alice());
@@ -1090,7 +1134,7 @@ async fn an_older_pending_ballot_never_hides_a_newer_confirmed_vote() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
// A Ğecko code shown, never paid; then a vote from the wallet, confirmed.
c.ballot(&doc, &[(&i1, Some("against"))], None).await;
let newer = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
@@ -1113,7 +1157,7 @@ async fn a_comment_counts_once_its_block_is_final() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
let r = c.ballot(&doc, &[(&i1, Some("for"))], Some(BOB)).await;
indexer.finalized(Some(99));
@@ -1147,7 +1191,7 @@ async fn copies_never_hide_the_real_comment() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
for n in 0..120 {
indexer.pay(100 + n, comment_of(&b), Signer { account: CHARLIE, identity: Some((7, "Charlie", true)) });
@@ -1180,7 +1224,7 @@ async fn a_gecko_ballot_is_settled_by_the_background_task_even_at_its_end() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
age(&state, "expires_at", -5, id_of(&b)).await;
indexer.pay(100, comment_of(&b), alice());
@@ -1205,7 +1249,7 @@ async fn the_receipt_is_for_the_one_who_asked_and_proves_the_vote() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let (i1, e3) = (version(&doc, "I1", "inForce"), version(&doc, "E3", "0"));
let (i1, e3) = (version(&doc, "I1", "0"), version(&doc, "E3", "0"));
let b = c.ballot(&doc, &[(&i1, Some("for")), (&e3, Some("against"))], Some(ALICE_GENERIC)).await;
indexer.pay(100, comment_of(&b), alice());
ballots::sweep(&state).await.expect("sweep");
@@ -1264,7 +1308,7 @@ async fn a_name_known_only_from_a_chain_vote_stays_free() {
let (mut admin, state) = Client::new(chain_config(&indexer)).await;
admin.login_admin("Yvv").await;
let doc = admin.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
for (block, signer) in [(100, alice()), (101, Signer { account: CHARLIE, identity: Some((9003, "Charlie", true)) })]
{
let b = admin.ballot(&doc, &[(&i1, Some("for"))], None).await;
@@ -1289,7 +1333,7 @@ async fn a_name_known_only_from_a_chain_vote_stays_free() {
}
#[tokio::test]
async fn fifty_voices_on_the_chain_bring_an_order_and_a_title_into_force() {
async fn voices_on_the_chain_bring_an_order_then_a_title_into_force() {
// The modules (ADR 0009) are voted like articles: in a basket, in one transaction.
let indexer = Indexer::start().await;
let (mut c, state) = Client::new(chain_config(&indexer)).await;
@@ -1326,8 +1370,16 @@ async fn fifty_voices_on_the_chain_bring_an_order_and_a_title_into_force() {
.expect("title")
.to_owned();
let doc = c.doc("monnaie-libre").await;
let keys: Vec<String> = (1..=50u8).map(|n| address::encode(G1_PREFIX, &[n; 32])).collect();
let mut last = Value::Null;
// Each on the quorum of its level (ADR 0013): the order at level 2 (quorum 12) asks 9
// « pour », the title at level 4 (quorum 50) 43.
let mut needed = Vec::new();
for id in [&o, &t] {
let d = c.ok("GET", &format!("/api/proposals/{id}"), None).await;
needed.push(d["proposal"]["evaluation"]["yesNeeded"].as_u64().expect("yesNeeded"));
}
assert_eq!(needed, [9, 43]);
let keys: Vec<String> = (1..=43u8).map(|n| address::encode(G1_PREFIX, &[n; 32])).collect();
let mut sent = Vec::new();
for (n, account) in keys.iter().enumerate() {
let b = c.ballot(&doc, &[(&o, Some("for")), (&t, Some("for"))], None).await;
let index = 20_000 + u32::try_from(n).expect("n");
@@ -1336,22 +1388,26 @@ async fn fifty_voices_on_the_chain_bring_an_order_and_a_title_into_force() {
b["comment"].as_str().expect("c"),
Signer { account, identity: Some((index, "Membre", true)) },
);
last = b;
sent.push(b);
if n % 10 == 9 {
ballots::sweep(&state).await.expect("sweep");
}
}
ballots::sweep(&state).await.expect("sweep");
let s = c.status(&last).await;
assert_eq!(s["status"], "confirmed");
let mut entered: Vec<(&str, &str)> = s["entered"]
.as_array()
.expect("entered")
.iter()
.map(|e| (e["code"].as_str().unwrap_or_default(), e["kind"].as_str().unwrap_or_default()))
.collect();
entered.sort_unstable();
assert_eq!(entered, [("ordre.conclusion", "order"), ("titre", "title")], "{s}");
// What entered at each confirmation, in the order of the chain.
let mut entered: Vec<(usize, String, String)> = Vec::new();
for (n, b) in sent.iter().enumerate() {
let s = c.status(b).await;
assert_eq!(s["status"], "confirmed", "{s}");
for e in s["entered"].as_array().expect("entered") {
entered.push((n + 1, e["code"].as_str().expect("code").into(), e["kind"].as_str().expect("kind").into()));
}
}
assert_eq!(
entered,
[(9, "ordre.conclusion".into(), "order".into()), (43, "titre".into(), "title".into())],
"the ninth voice, then the forty-third"
);
let d = c.doc("monnaie-libre").await;
let (p1, p2) = (unit(&d, "K1")["position"].as_f64().expect("p"), unit(&d, "K2")["position"].as_f64().expect("p"));
assert!(p2 < p1, "K2 now reads before K1");
@@ -1380,7 +1436,7 @@ async fn without_a_vote_account_monnaie_libre_votes_in_simulation_and_the_test_g
assert!(doc["vote"]["chain"].is_null());
assert_eq!(doc["canVote"], true);
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let (s, v) = c
.call(
"POST",
@@ -1392,7 +1448,7 @@ async fn without_a_vote_account_monnaie_libre_votes_in_simulation_and_the_test_g
let essai = c.doc("essai").await;
assert_eq!((essai["vote"]["way"].as_str(), essai["vote"]["open"].as_bool()), (Some("server"), Some(true)));
assert_eq!(essai["vote"]["simulated"], false);
let p = version(&essai, "I1", "inForce");
let p = version(&essai, "I1", "0");
c.ok("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
let essai = c.doc("essai").await;
assert_eq!(mine(&essai, &p), (Some("for"), Some("recorded")));
@@ -1439,9 +1495,9 @@ async fn what_a_ballot_may_not_hold() {
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
let essai = c.doc("essai").await;
let elsewhere = version(&essai, "I1", "inForce");
let elsewhere = version(&essai, "I1", "0");
for (lines, author, code) in [
(json!([]), Value::Null, "lines"),
(
@@ -1478,6 +1534,9 @@ async fn a_grouped_vote_in_a_managed_group_is_one_write() {
a.login("Yvv").await;
let mut b = a.other();
b.login("Chiara").await;
for code in ["E4", "E5", "E6"] {
two_voices(&state, "essai", code).await;
}
let doc = a.doc("essai").await;
let id = doc["doc"]["id"].as_str().expect("id").to_owned();
let (e4, e5, e6) = (version(&doc, "E4", "0"), version(&doc, "E5", "0"), version(&doc, "E6", "0"));
@@ -1486,7 +1545,7 @@ async fn a_grouped_vote_in_a_managed_group_is_one_write() {
]);
let v = a.ok("POST", &format!("/api/docs/{id}/votes"), Some(json!({ "lines": lines }))).await;
assert_eq!(v["count"], 3);
assert!(v["entered"].as_array().expect("e").is_empty(), "one voice never makes a text pass");
assert!(v["entered"].as_array().expect("e").is_empty(), "one voice of two, on a quorum of 2");
// All or nothing: one line that does not hold, and no line is written.
let (s, _) = b
.call(
@@ -1528,7 +1587,7 @@ async fn a_simulated_vote_in_monnaie_libre_is_seen_at_once_in_my_vote_and_the_to
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let (i1, e3, e4) = (version(&doc, "I1", "inForce"), version(&doc, "E3", "0"), version(&doc, "E4", "0"));
let (i1, e3, e4) = (version(&doc, "I1", "0"), version(&doc, "E3", "0"), version(&doc, "E4", "0"));
// One vote, without a wallet: written by the server, simulated.
let v = c.ok("POST", &format!("/api/proposals/{i1}/vote"), Some(json!({ "choice": "for" }))).await;
assert_eq!((v["simulated"].as_bool(), v["entered"].as_array().map(Vec::len)), (Some(true), Some(0)));
@@ -1591,18 +1650,7 @@ async fn shown_but_never_adopted_when_simulated_votes_are_switched_off_for_adopt
a.login("Anna").await;
let doc = a.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
// A small floor and a small web, for the test only: two voices may pass.
sqlx::query("UPDATE documents SET vote_floor = 1 WHERE id = ?")
.bind(&doc_id)
.execute(&state.db)
.await
.expect("floor");
sqlx::query(
"INSERT INTO settings (key, value, updated_at) VALUES ('wot_size', '2', '2026-11-02T00:00:00Z') ON CONFLICT(key) DO UPDATE SET value = '2'",
)
.execute(&state.db)
.await
.expect("web");
small_web(&state, &doc_id).await;
let e4 = version(&doc, "E4", "0");
let mut b = a.other();
b.login("Bruno").await;
@@ -1626,14 +1674,10 @@ async fn opening_the_chain_sets_nothing_aside_the_act_does_and_examines_again_wh
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
// A small floor and a small web, for the test only: two voices pass, one never does.
sqlx::query("UPDATE documents SET vote_floor = 1 WHERE id = ?").bind(&doc_id).execute(&state.db).await.expect("floor");
sqlx::query("INSERT INTO settings (key, value, updated_at) VALUES ('wot_size', '2', '2026-11-02T00:00:00Z') ON CONFLICT(key) DO UPDATE SET value = '2'")
.execute(&state.db)
.await
.expect("web");
small_web(&state, &doc_id).await;
let (e4, e5, e6) = (version(&doc, "E4", "0"), version(&doc, "E5", "0"), version(&doc, "E6", "0"));
let seed_i1 = version(&doc, "I1", "inForce");
// The seed proposal of I1 (ADR 0011), and a rival written before the votes.
let seed_i1 = version(&doc, "I1", "0");
let i1_unit = unit(&doc, "I1")["id"].as_str().expect("unit").to_owned();
let rival = c
.ok(
@@ -1645,18 +1689,26 @@ async fn opening_the_chain_sets_nothing_aside_the_act_does_and_examines_again_wh
.as_str()
.expect("rival")
.to_owned();
// The chain open: one proven voice on E5, one on E6 — not enough to enter.
for (p, block, signer) in [(&e5, 100, alice()), (&e6, 101, Signer { account: CHARLIE, identity: Some((9003, "Charlie", true)) })] {
let b = c.ballot(&doc, &[(p, Some("for"))], None).await;
// The chain open: one proven voice on E5, one on E6 — not enough to enter; two on
// the seed proposal of I1, which enters on proven votes alone.
let charlie = Signer { account: CHARLIE, identity: Some((9003, "Charlie", true)) };
for (p, block, signer) in [(&e5, 100, alice()), (&e6, 101, charlie)] {
let b = c.ballot(&doc, &[(p, Some("for")), (&seed_i1, Some("for"))], None).await;
indexer.pay(block, comment_of(&b), signer);
}
ballots::sweep(&state).await.expect("sweep");
// The chain closed: Luna and Bruno vote in simulation; E4, E5, E6 and the rival of I1 enter.
let d = c.doc("monnaie-libre").await;
assert_eq!(unit(&d, "I1")["inForce"]["id"], seed_i1.as_str());
assert!(unit(&d, "E5")["inForce"].is_null() && unit(&d, "E6")["inForce"].is_null());
// The chain closed: Luna, Bruno and Marc vote in simulation; E4, E5, E6 enter, and
// the rival of I1, three voices against two, takes the place of the seed proposal.
c.ok("POST", "/api/dev/vote-chain", Some(json!({ "open": false }))).await;
let mut luna = c.other();
luna.login("Luna").await;
let mut bruno = c.other();
bruno.login("Bruno").await;
let mut marc = c.other();
marc.login("Marc").await;
let lines = json!([
{ "proposalId": e4, "choice": "for" }, { "proposalId": e5, "choice": "for" },
{ "proposalId": e6, "choice": "for" }, { "proposalId": rival, "choice": "for" },
@@ -1664,6 +1716,7 @@ async fn opening_the_chain_sets_nothing_aside_the_act_does_and_examines_again_wh
luna.ok("POST", &format!("/api/docs/{doc_id}/votes"), Some(json!({ "lines": lines }))).await;
let lines = json!([{ "proposalId": e4, "choice": "for" }, { "proposalId": rival, "choice": "for" }]);
bruno.ok("POST", &format!("/api/docs/{doc_id}/votes"), Some(json!({ "lines": lines }))).await;
marc.ok("POST", &format!("/api/proposals/{rival}/vote"), Some(json!({ "choice": "for" }))).await;
let d = c.doc("monnaie-libre").await;
for (code, id) in [("E4", &e4), ("E5", &e5), ("E6", &e6), ("I1", &rival)] {
assert_eq!(unit(&d, code)["inForce"]["id"], id.as_str(), "{code}");
@@ -1681,7 +1734,7 @@ async fn opening_the_chain_sets_nothing_aside_the_act_does_and_examines_again_wh
ballots::sweep(&state).await.expect("sweep");
// Told first, nothing changed.
let told = ld_server::simulation::discard(&state, false).await.expect("dry run");
assert_eq!((told.applied, told.ballots, told.votes), (false, 2, 6));
assert_eq!((told.applied, told.ballots, told.votes), (false, 3, 7));
let verdicts = |list: &[ld_server::simulation::Reexamined]| {
let mut v: Vec<(String, bool, bool)> = list.iter().map(|r| (r.code.clone(), r.holds, r.returned)).collect();
v.sort();
@@ -1715,9 +1768,12 @@ async fn opening_the_chain_sets_nothing_aside_the_act_does_and_examines_again_wh
.await
.expect("left");
assert_eq!(left, (0, 0));
let adopted: Vec<String> =
let mut adopted: Vec<String> =
sqlx::query_scalar("SELECT proposal_id FROM adoptions").fetch_all(&state.db).await.expect("adoptions");
assert_eq!(adopted, [e6.clone()], "the adoptions that do not hold are void");
adopted.sort();
let mut kept = vec![seed_i1.clone(), e6.clone()];
kept.sort();
assert_eq!(adopted, kept, "the adoptions that do not hold are void; those on proven votes alone stay");
// Once is enough.
let again = ld_server::simulation::discard(&state, true).await.expect("again");
assert_eq!((again.ballots, again.reexamined.len()), (0, 0));
@@ -1735,7 +1791,7 @@ async fn an_intention_never_confirmed_is_set_aside_and_told() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
// A vote proven first: « contre ».
let first = c.ballot(&doc, &[(&i1, Some("against"))], None).await;
indexer.pay(100, comment_of(&first), alice());
@@ -1786,7 +1842,7 @@ async fn two_votes_of_one_identity_from_two_sessions_the_chain_keeps_the_last()
let mut other = chiara.other();
other.login("Yvv").await;
let doc = chiara.doc("monnaie-libre").await;
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
// One person, one wallet, two sessions: « pour » here, « contre » there.
let a = chiara.ballot(&doc, &[(&i1, Some("for"))], None).await;
let b = other.ballot(&doc, &[(&i1, Some("against"))], None).await;
@@ -1827,7 +1883,7 @@ async fn a_for_proven_while_an_objection_holds_the_version_is_set_aside() {
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let (e1, e5) = (version(&doc, "E1", "inForce"), version(&doc, "E5", "0"));
let (e1, e5) = (version(&doc, "E1", "0"), version(&doc, "E5", "0"));
// Proven first: « contre » on E5.
let before = c.ballot(&doc, &[(&e5, Some("against"))], None).await;
indexer.pay(100, comment_of(&before), alice());
@@ -1873,7 +1929,7 @@ async fn a_waiting_intention_counts_as_my_vote_in_decisions() {
let doc = c.doc("monnaie-libre").await;
let without = |d: &Value| d["docs"][0]["withoutMyVote"].as_u64();
let before = without(&c.ok("GET", "/api/groups/monnaie-libre/decisions", None).await);
let i1 = version(&doc, "I1", "inForce");
let i1 = version(&doc, "I1", "0");
c.ballot(&doc, &[(&i1, Some("for"))], None).await;
let after = without(&c.ok("GET", "/api/groups/monnaie-libre/decisions", None).await);
assert_eq!(after, before.map(|n| n - 1));