Lot S, phase 2 : le quorum dans le serveur (ADR 0013), vu rouge puis vert
- migration 0011 (additive ; 0010 reste au lot V) : documents.quorums (par cran,
NULL = vote_floor à chaque cran, le C d'avant), units.quorum (NULL = celui du
cran), journaux quorum_changes et quorum_amendments ;
- quorum effectif = units.quorum ?? quorums[cran] ; les propositions d'inertie et
les modules en héritent ; p0 = 2 × vote_floor / W, jamais le quorum d'un article ;
- route PUT /units/{id}/quorum { "quorum": n | null } : trustin_access::sets_quorum
(l'auteur de la première version, un administrateur pour le seed), refusée dès
qu'un vote existe (quorum::voted_units : vote du serveur, bulletin de la chaîne
en attente ou confirmé, adoption) ; bornes 2..W, 1..W dans le groupe d'essai ;
- amendement gardé 2026-11-03-quorums-par-cran : écrit les quorums du seed là où
il n'y en a pas, une fois par document, consigné, file de publication là où il
publie ;
- JSON : evaluation.quorum / yesNeeded / noNeeded ; l'unité : quorum,
quorumDefault, canSetQuorum ; doc.quorums ; levels[].quorum ; document.json
publié : quorums ; adoptions.params : quorum (plus de constant) ;
- seed : quorums 12 · 12 · 50 · 50 · 50 (Monnaie libre), 1 partout (essai).
- tests : tests/quorum.rs (5) et les règles pures ; les scénarios du groupe
d'essai qui demandaient deux voix posent le quorum 2 de l'article, comme son
auteur le ferait ; les chiffres de Monnaie libre suivent (9 au cran 2, 39 au
cran 3, 43 au cran 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,22 @@ async fn doc_id(c: &mut Client, group: &str) -> String {
|
||||
v["docs"][0]["id"].as_str().expect("doc").to_owned()
|
||||
}
|
||||
|
||||
/// 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}");
|
||||
}
|
||||
|
||||
fn unit<'a>(doc: &'a Value, code: &str) -> &'a Value {
|
||||
doc["units"].as_array().expect("units").iter().find(|u| u["code"] == code).expect("unit")
|
||||
}
|
||||
@@ -150,15 +166,15 @@ async fn adoption_in_the_test_group_and_publication_queued_only_where_published(
|
||||
a.login("Yvv", false).await;
|
||||
let mut b = a.with(None);
|
||||
b.login("Chiara", false).await;
|
||||
// Dev identities are members of every group: W(essai) = 2, C = 1.
|
||||
// Dev identities are members of every group: W(essai) = 2, the quorum 1 at every level
|
||||
// (ADR 0013): one voice passes in the test group.
|
||||
let id = doc_id(&mut a, "essai").await;
|
||||
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
||||
let e3 = unit(&doc, "E3");
|
||||
assert_eq!(e3["proposals"][0]["evaluation"]["quorum"], 1);
|
||||
let p = e3["proposals"][0]["id"].as_str().expect("p").to_owned();
|
||||
let (_, v) = a.call("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||
assert!(v["entered"].as_array().expect("e").is_empty(), "one of two is not enough");
|
||||
let (_, v) = b.call("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||
assert_eq!(v["entered"][0]["code"], "E3");
|
||||
assert_eq!(v["entered"][0]["code"], "E3", "one voice in the test group");
|
||||
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
||||
let e3 = unit(&doc, "E3");
|
||||
assert_eq!(e3["inForce"]["id"], p.as_str());
|
||||
@@ -191,9 +207,11 @@ async fn a_rival_replaces_the_seed_and_the_seed_returns_among_proposals() {
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
let rival = v["id"].as_str().expect("id").to_owned();
|
||||
a.call("POST", &format!("/api/proposals/{rival}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||
let (_, v) = b.call("POST", &format!("/api/proposals/{rival}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||
// The quorum 1 of the test group (ADR 0013): its *for* against nothing replaces the seed.
|
||||
let (_, v) = a.call("POST", &format!("/api/proposals/{rival}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||
assert_eq!(v["entered"][0]["kind"], "text");
|
||||
let (_, v) = b.call("POST", &format!("/api/proposals/{rival}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||
assert!(v["entered"].as_array().expect("e").is_empty(), "already in force");
|
||||
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
||||
let e1 = unit(&doc, "E1");
|
||||
assert_eq!(e1["inForce"]["id"], rival.as_str());
|
||||
@@ -204,7 +222,7 @@ async fn a_rival_replaces_the_seed_and_the_seed_returns_among_proposals() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn editing_is_not_voting() {
|
||||
let (mut a, _) = Client::production().await;
|
||||
let (mut a, state) = Client::production().await;
|
||||
// An administrator of trustinWallet answers for the starting texts (no author).
|
||||
a.login("Yvv", true).await;
|
||||
let mut b = a.with(None);
|
||||
@@ -315,6 +333,8 @@ async fn editing_is_not_voting() {
|
||||
let essai = doc_id(&mut a, "essai").await;
|
||||
let (_, essai_doc) = a.call("GET", &format!("/api/docs/{essai}"), None).await;
|
||||
let uid = unit(&essai_doc, "E2")["id"].as_str().expect("u").to_owned();
|
||||
// Votes stay on it while it is edited: one voice of two does not make it pass.
|
||||
two_voices(&state, "essai", "E2").await;
|
||||
let (_, v) = a
|
||||
.call("POST", &format!("/api/docs/{essai}/proposals"), Some(json!({
|
||||
"unitId": uid, "title": "Réciprocité", "body": "Je m'engage à ne certifier que des persones qui respectent ces engagements.", "submit": true,
|
||||
@@ -362,10 +382,11 @@ async fn editing_is_not_voting() {
|
||||
/// the full cycle: tests/objections.rs).
|
||||
#[tokio::test]
|
||||
async fn an_objection_holds_back_the_version_that_is_not_in_force() {
|
||||
let (mut a, _) = Client::production().await;
|
||||
let (mut a, state) = Client::production().await;
|
||||
a.login("Yvv", false).await;
|
||||
let mut b = a.with(None);
|
||||
b.login("Chiara", false).await;
|
||||
two_voices(&state, "essai", "K1").await;
|
||||
let id = doc_id(&mut a, "essai").await;
|
||||
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
||||
let e2 = unit(&doc, "E2")["inForce"]["id"].as_str().expect("e2").to_owned();
|
||||
@@ -1157,7 +1178,10 @@ async fn a_seed_amendment_leaves_a_touched_preambule_alone() {
|
||||
let i1 = unit(&doc, "I1")["inForce"].clone();
|
||||
let pid = i1["id"].as_str().expect("id").to_owned();
|
||||
let (s, v) = match touch {
|
||||
// A vote that does not confirm it (the quorum 2: one voice is not enough), so
|
||||
// the refusal says the vote itself, not a confirmation.
|
||||
Touch::Vote => {
|
||||
two_voices(&before, "essai", "I1").await;
|
||||
c.call("POST", &format!("/api/proposals/{pid}/vote"), Some(json!({ "choice": "for" }))).await
|
||||
}
|
||||
// Since 2026-10-31 it waits for an administrator: a fix waiting is a trace too.
|
||||
|
||||
Reference in New Issue
Block a user