v3 : sur une version retenue, « contre » et retrait oui, « pour » non (moteur, serveur, chaîne)

Décision d'Yvv du 01/11/2026 : « Retirer ou "contre" ». Le moteur dit la règle
(objection::may_cast) ; le vote direct, le vote groupé et le bulletin de la chaîne
l'appliquent par DocState::admits : « pour » 409 objection, « contre » et retrait
acceptés, une ligne refusée refuse tout le vote. Un bulletin scellé avant
l'objection compte tel qu'il est signé ; une version retenue n'entre jamais.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-02 00:03:45 +01:00
co-authored by Claude Opus 5.5
parent 61547c11c0
commit 238c7db93f
8 changed files with 222 additions and 19 deletions
+46
View File
@@ -776,6 +776,52 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
assert!(unit(&d, "E5")["inForce"].is_null(), "an objection holds it back");
}
/// Yvv, 2026-11-01: « Retirer ou "contre" ». On the chain too, a held version takes a
/// ballot against it or a withdrawal, never a *for* — alone or among free lines.
#[tokio::test]
async fn on_the_chain_a_held_version_takes_against_and_withdrawal_never_for() {
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 (e1, e4, e5) = (version(&doc, "E1", "inForce"), version(&doc, "E4", "0"), version(&doc, "E5", "0"));
let o = c
.ok(
"POST",
&format!("/api/proposals/{e5}/objections"),
Some(json!({ "otherId": e1, "kind": "contredit", "reason": "Ces deux textes disent le contraire." })),
)
.await;
assert_eq!(o["heldId"], e5.as_str());
let uri = format!("/api/docs/{doc_id}/ballots");
let refused = (StatusCode::CONFLICT, Some("objection"));
for lines in [
json!([{ "proposalId": e5, "choice": "for" }]),
json!([{ "proposalId": e4, "choice": "for" }, { "proposalId": e5, "choice": "for" }]),
] {
let (s, v) = c.call("POST", &uri, Some(json!({ "lines": lines }))).await;
assert_eq!((s, v["error"].as_str()), refused, "{lines}");
}
// Against the held one, for a free one: sealed, then confirmed by the chain.
let b = c.ballot(&doc, &[(&e4, Some("for")), (&e5, Some("against"))], None).await;
indexer.pay(200, comment_of(&b), alice());
ballots::sweep(&state).await.expect("sweep");
assert_eq!(c.status(&b).await["status"], "confirmed");
let d = c.doc("monnaie-libre").await;
assert_eq!((tally(&d, &e4), tally(&d, &e5)), ((1, 0), (0, 1)));
assert_eq!(find(&d, &e5)["myVote"], "against");
// A withdrawal: the last ballot stands.
let w = c.ballot(&doc, &[(&e5, None)], None).await;
indexer.pay(201, comment_of(&w), alice());
ballots::sweep(&state).await.expect("sweep");
assert_eq!(c.status(&w).await["status"], "confirmed");
let d = c.doc("monnaie-libre").await;
assert_eq!(tally(&d, &e5), (0, 0));
assert!(find(&d, &e5)["myVote"].is_null());
assert!(unit(&d, "E5")["inForce"].is_null(), "held, it never enters");
}
// ---------------------------------------------------------------------------
// After the independent review of 74f96be (lot B2): its probes, kept as tests
// ---------------------------------------------------------------------------