Lot V : le quorum jugé sur le décompte affiché, l'adoption jamais sur une intention ; « un vote existe » pour chaque état du tampon (en attente, prouvé quel que soit son sort, retrait, simulé) — tests vus rouges par mutation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 05:37:46 +01:00
co-authored by Claude Opus 5.5
parent c0c4d129cb
commit 0cde5bdbd0
2 changed files with 158 additions and 6 deletions
+141
View File
@@ -1934,3 +1934,144 @@ async fn a_waiting_intention_counts_as_my_vote_in_decisions() {
let after = without(&c.ok("GET", "/api/groups/monnaie-libre/decisions", None).await);
assert_eq!(after, before.map(|n| n - 1));
}
/// The evaluation as the engine gives it for these totals, on this version's letters.
async fn judged(state: &AppState, doc: &Value, id: &str, shown: (u32, u32)) -> Value {
let st = ld_server::docs::load_state(state, doc["doc"]["id"].as_str().expect("doc")).await.expect("state");
let p = st.proposals.iter().find(|p| p.id == id).expect("version");
let u = st.units.iter().find(|u| u.id == p.unit_id).expect("unit");
let tally = ld_engine::threshold::Tally { for_votes: shown.0, against: shown.1 };
let e = ld_engine::threshold::evaluate(tally, st.electorate, &st.params(u, p)).expect("evaluation");
serde_json::to_value(e).expect("json")
}
/// What adoption judges: never an intention, simulated votes by default (ADR 0012).
async fn adopted_evaluation(state: &AppState, doc: &Value, id: &str) -> Value {
let st = ld_server::docs::load_state(state, doc["doc"]["id"].as_str().expect("doc")).await.expect("state");
let p = st.proposals.iter().find(|p| p.id == id).expect("version");
let u = st.units.iter().find(|u| u.id == p.unit_id).expect("unit");
serde_json::to_value(st.evaluation(u, p).expect("evaluation")).expect("json")
}
/// The quorum (ADR 0013) is judged on the totals shown — votes recorded, pending and
/// simulated, the vote one sees as one's own replaced by one's intention: `quorum`,
/// `yesNeeded`, `noNeeded` and `missing` are those of the totals shown. Adoption counts
/// the proven votes and the simulated ones (`SIMULATED_ADOPTS`), never an intention.
#[tokio::test]
async fn the_quorum_is_judged_on_the_totals_shown_and_adoption_never_on_an_intention() {
let indexer = Indexer::start().await;
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
// I1, a seed proposal at level 2: the quorum 12, 9 « pour » at no vote.
let i1 = version(&doc, "I1", "0");
assert_eq!(find(&doc, &i1)["evaluation"], judged(&state, &doc, &i1, (0, 0)).await);
assert_eq!(find(&doc, &i1)["evaluation"]["yesNeeded"], 9);
// Proven: « contre ». Then « pour », sealed, not yet proven: it takes the place of
// « contre » in the totals shown, and in the evaluation shown.
let first = c.ballot(&doc, &[(&i1, Some("against"))], None).await;
indexer.pay(100, comment_of(&first), alice());
ballots::sweep(&state).await.expect("sweep");
c.ballot(&doc, &[(&i1, Some("for"))], None).await;
let d = c.doc("monnaie-libre").await;
assert_eq!((tally(&d, &i1), pending(&d, &i1), counted(&state, &d, &i1).await), ((1, 0), (1, 0), (0, 1)));
let shown = find(&d, &i1)["evaluation"].clone();
assert_eq!(shown, judged(&state, &d, &i1, (1, 0)).await, "on the totals shown");
assert_eq!((shown["cast"].as_u64(), shown["missing"].as_u64()), (Some(1), Some(8)));
let adoption = adopted_evaluation(&state, &d, &i1).await;
assert_eq!(adoption, judged(&state, &d, &i1, (0, 1)).await, "adoption: the proven « contre » only");
assert!(adoption["missing"].as_u64() > Some(8), "the intention never brings adoption closer: {adoption}");
// The detail says the same, and its five levels are asked at the votes shown.
let detail = c.ok("GET", &format!("/api/proposals/{i1}"), None).await;
assert_eq!(detail["proposal"]["evaluation"], shown);
assert!(detail["levels"].as_array().expect("levels").iter().all(|l| l["cast"] == 1), "{detail}");
// The chain closed: a simulated « pour » counts in both, like a proven vote.
c.ok("POST", "/api/dev/vote-chain", Some(json!({ "open": false }))).await;
let mut luna = c.other();
luna.login("Luna").await;
luna.ok(
"POST",
&format!("/api/docs/{doc_id}/votes"),
Some(json!({ "lines": [{ "proposalId": i1, "choice": "for" }] })),
)
.await;
let d = c.doc("monnaie-libre").await;
assert_eq!((tally(&d, &i1), pending(&d, &i1), counted(&state, &d, &i1).await), ((2, 0), (1, 0), (1, 1)));
assert_eq!(find(&d, &i1)["evaluation"], judged(&state, &d, &i1, (2, 0)).await);
assert_eq!(adopted_evaluation(&state, &d, &i1).await, judged(&state, &d, &i1, (1, 1)).await);
}
/// « Un vote existe » (ADR 0013, `quorum::voted_units`), for every state of the buffer
/// (ADR 0012): an intention still pending, a proven line whatever became of it (a
/// withdrawal, a vote overtaken, a *for* set aside by an objection), a simulated vote,
/// a simulated withdrawal — the history of the ballots keeps their trace. A ballot the
/// wallet never signed, one never seen on the chain in time, one the chain refused, is
/// no vote.
#[tokio::test]
async fn a_vote_exists_for_every_state_of_the_buffer() {
let indexer = Indexer::start().await;
let (mut c, state) = Client::new(chain_config(&indexer)).await;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let unit_id = |code: &str| unit(&doc, code)["id"].as_str().expect("unit").to_owned();
let exists = |u: String| {
let state = state.clone();
async move { ld_server::quorum::a_vote_exists(&state, &u).await.expect("query") }
};
let (i1, e1, e2, e3, e4) = (
version(&doc, "I1", "0"),
version(&doc, "E1", "0"),
version(&doc, "E2", "0"),
version(&doc, "E3", "0"),
version(&doc, "E4", "0"),
);
for code in ["I1", "E1", "E2", "E3", "E4"] {
assert!(!exists(unit_id(code)).await, "{code}: nobody voted yet");
}
// A ballot the wallet never signed: nothing.
let dropped = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
assert!(exists(unit_id("I1")).await, "an intention pending is a vote already");
c.ok("DELETE", &format!("/api/ballots/{}", id_of(&dropped)), None).await;
assert!(!exists(unit_id("I1")).await, "let go at once: no vote");
// Never seen on the chain in time: no vote.
let late = c.ballot(&doc, &[(&e2, Some("for"))], None).await;
age(&state, "expires_at", -2 * 3600, id_of(&late)).await;
ballots::sweep(&state).await.expect("sweep");
assert_eq!(db_status(&state, id_of(&late)).await.0, "expired");
assert!(!exists(unit_id("E2")).await);
// Proven, then withdrawn on the chain: no row in `votes`, the trace stays.
let a = c.ballot(&doc, &[(&e1, Some("for"))], None).await;
indexer.pay(100, comment_of(&a), alice());
ballots::sweep(&state).await.expect("sweep");
let w = c.ballot(&doc, &[(&e1, None)], None).await;
indexer.pay(101, comment_of(&w), alice());
ballots::sweep(&state).await.expect("sweep");
assert_eq!(counted(&state, &c.doc("monnaie-libre").await, &e1).await, (0, 0));
assert!(exists(unit_id("E1")).await, "a vote withdrawn leaves its trace");
// The chain closed: a simulated vote; a simulated vote, then withdrawn.
c.ok("POST", "/api/dev/vote-chain", Some(json!({ "open": false }))).await;
c.ok("POST", &format!("/api/proposals/{e3}/vote"), Some(json!({ "choice": "against" }))).await;
assert!(exists(unit_id("E3")).await, "a simulated vote");
c.ok("POST", &format!("/api/proposals/{e4}/vote"), Some(json!({ "choice": "for" }))).await;
c.ok("POST", &format!("/api/proposals/{e4}/vote"), Some(json!({ "choice": null }))).await;
assert_eq!(counted(&state, &c.doc("monnaie-libre").await, &e4).await, (0, 0));
assert!(exists(unit_id("E4")).await, "a simulated vote withdrawn leaves its trace");
// The quorum is held: the author (an administrator, for a text of the seed) may not set it.
let mut admin = c.other();
admin.login_admin("Yvv").await;
for code in ["E1", "E3", "E4"] {
let (s, v) =
admin.call("PUT", &format!("/api/units/{}/quorum", unit_id(code)), Some(json!({ "quorum": 20 }))).await;
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("voted")), "{code}");
}
let (s, _) =
admin.call("PUT", &format!("/api/units/{}/quorum", unit_id("E2")), Some(json!({ "quorum": 20 }))).await;
assert_eq!(s, StatusCode::OK, "E2: no vote");
// The act « supprime » sets every simulated vote aside: with them, their trace.
ld_server::simulation::discard(&state, true).await.expect("act");
assert!(!exists(unit_id("E3")).await && !exists(unit_id("E4")).await);
assert!(exists(unit_id("E1")).await, "a proven trace stays");
let _ = doc_id;
}