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
+38 -13
View File
@@ -14,10 +14,14 @@ struct Client {
impl Client {
async fn new() -> Self {
Self::with_state().await.0
}
async fn with_state() -> (Self, AppState) {
let db = open_db("sqlite::memory:").await.expect("db");
let state = AppState::new(Config::for_tests(), db).await.expect("state");
seed::ensure(&state).await.expect("seed");
Self { router: app(state), cookie: None }
(Self { router: app(state.clone()), cookie: None }, state)
}
fn other(&self) -> Self {
@@ -91,6 +95,21 @@ fn version(doc: &Value, code: &str, which: &str) -> String {
v["id"].as_str().expect("id").to_owned()
}
/// The quorum of an article of the test group set to 2, as its author — an administrator,
/// for a text of the seed — sets it before the first vote (ADR 0013): 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, code: &str) {
let n = sqlx::query(
"UPDATE units SET quorum = 2 WHERE code = ? AND doc_id IN (SELECT id FROM documents WHERE group_slug = 'essai')",
)
.bind(code)
.execute(&state.db)
.await
.expect("quorum")
.rows_affected();
assert_eq!(n, 1, "{code}");
}
async fn decisions(c: &mut Client, group: &str) -> Value {
c.get(&format!("/api/groups/{group}/decisions")).await
}
@@ -139,28 +158,33 @@ async fn a_group_one_does_not_open_shows_nothing_of_what_is_coming() {
async fn decisions_one_line_per_doc_with_the_modules_left_to_vote() {
// In the test group, which votes on the server (Monnaie libre votes on the chain:
// tests/ballots.rs counts « sans ton vote » there).
let mut c = Client::new().await;
let (mut c, state) = Client::with_state().await;
c.login("Chiara", false).await;
// Someone else, who will not vote: W(essai) = 2.
let mut yvv = c.other();
yvv.login("Yvv", false).await;
let d = decisions(&mut c, "essai").await;
assert_eq!(d["canVote"], true);
let docs = d["docs"].as_array().expect("docs");
assert_eq!(docs.len(), 1);
assert_eq!(docs[0]["slug"], "acte-engagement-certification");
assert_eq!(docs[0]["title"]["fr"], "Acte d'engagement Certification");
// 3 seeds a minima + 30 open articles, the two variants counting once; the title of the
// document and the order of 7 sections (« ordonnancement » holds one article: nothing to decide).
// 3 seed proposals (ADR 0011) + 30 open articles, the two variants counting once; the title
// of the document and the order of 7 sections (« ordonnancement » holds one article: nothing
// to decide).
assert_eq!(docs[0]["toVote"], 41);
assert_eq!(docs[0]["withoutMyVote"], 41);
let doc = the_doc(&mut c, "essai").await;
// Votes that adopt nothing: the quorum 2 (ADR 0013), one voice of two.
two_voices(&state, "E3").await;
two_voices(&state, "I1").await;
c.vote(&version(&doc, "E3", "0"), "for").await;
c.vote(&version(&doc, "E3", "1"), "against").await;
c.vote(&version(&doc, "I1", "inForce"), "for").await;
c.vote(&version(&doc, "I1", "0"), "for").await;
let d = decisions(&mut c, "essai").await;
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(41), Some(39)));
// Someone else has not voted yet.
let mut yvv = c.other();
yvv.login("Yvv", false).await;
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 41);
}
@@ -170,11 +194,11 @@ async fn an_adopted_module_leaves_the_count_and_a_vote_to_reconfirm_does_not_cou
yvv.login("Yvv", false).await;
let mut chiara = yvv.other();
chiara.login("Chiara", false).await;
// Dev identities are members of every group: W(essai) = 2, C = 1; two « for » adopt.
// Dev identities are members of every group: W(essai) = 2, the quorum 1 at every level
// (ADR 0013): one « for » adopts.
let doc = the_doc(&mut yvv, "essai").await;
let e3 = version(&doc, "E3", "0");
yvv.vote(&e3, "for").await;
let v = chiara.vote(&e3, "for").await;
let v = yvv.vote(&e3, "for").await;
assert_eq!(v["entered"][0]["code"], "E3");
let d = decisions(&mut yvv, "essai").await;
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(40), Some(40)));
@@ -239,15 +263,16 @@ async fn a_version_held_by_an_objection_is_not_to_vote_while_it_is_held() {
}
#[tokio::test]
async fn where_one_does_not_vote_there_is_no_count_of_ones_own() {
async fn while_the_chain_is_closed_a_guest_votes_in_simulation_and_has_a_count_of_ones_own() {
let mut admin = Client::new().await;
admin.login("Yvv", true).await;
let mut guest = admin.other();
guest.invited(&mut admin, &["monnaie-libre", "essai"]).await;
// No vote account: Monnaie libre votes in simulation, for whoever opens it (ADR 0012).
let d = decisions(&mut guest, "monnaie-libre").await;
assert_eq!(d["canVote"], false);
assert_eq!(d["canVote"], true);
assert_eq!(d["docs"][0]["toVote"], 41);
assert!(d["docs"][0]["withoutMyVote"].is_null());
assert_eq!(d["docs"][0]["withoutMyVote"], 41);
// In the test group, the same person votes.
let d = decisions(&mut guest, "essai").await;
assert_eq!(d["canVote"], true);