forked from yvv/decision
WIP lot V : le tampon câblé dans le serveur (migration 0010, simulation, discard-simulation), tests en cours de reprise — repris tel quel après la coupure du 02/11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -291,10 +291,43 @@ fn find<'a>(doc: &'a Value, id: &str) -> &'a Value {
|
||||
}
|
||||
|
||||
fn tally(doc: &Value, id: &str) -> (u64, u64) {
|
||||
let t = &find(doc, id)["tally"];
|
||||
pair(&find(doc, id)["tally"])
|
||||
}
|
||||
|
||||
fn pair(t: &Value) -> (u64, u64) {
|
||||
(t["for"].as_u64().expect("for"), t["against"].as_u64().expect("against"))
|
||||
}
|
||||
|
||||
/// The part of the totals shown still waiting for its proof (ADR 0012).
|
||||
fn pending(doc: &Value, id: &str) -> (u64, u64) {
|
||||
pair(&find(doc, id)["tallyPending"])
|
||||
}
|
||||
|
||||
/// What adoption counts: never an intention without proof.
|
||||
async fn counted(state: &AppState, doc: &Value, id: &str) -> (u32, u32) {
|
||||
let st = ld_server::docs::load_state(state, doc["doc"]["id"].as_str().expect("doc")).await.expect("state");
|
||||
let t = st.tally(id);
|
||||
(t.for_votes, t.against)
|
||||
}
|
||||
|
||||
/// « Ton vote » and where it stands.
|
||||
fn mine<'a>(doc: &'a Value, id: &str) -> (Option<&'a str>, Option<&'a str>) {
|
||||
let p = find(doc, id);
|
||||
(p["myVote"].as_str(), p["myVoteState"].as_str())
|
||||
}
|
||||
|
||||
/// The messages of a session about its ballots (ADR 0012): (kind, ballot id).
|
||||
async fn ballot_messages(c: &mut Client) -> Vec<(String, String)> {
|
||||
let m = c.ok("GET", "/api/messages", None).await;
|
||||
m["messages"]
|
||||
.as_array()
|
||||
.expect("messages")
|
||||
.iter()
|
||||
.filter(|m| m["topic"] == "ballot")
|
||||
.map(|m| (m["kind"].as_str().expect("kind").to_owned(), m["ballot"]["id"].as_str().expect("id").to_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn rewind_check(state: &AppState) {
|
||||
// The page asks often, the indexer is read at most every few seconds: tests rewind that clock.
|
||||
sqlx::query("UPDATE ballots SET checked_at = NULL").execute(&state.db).await.expect("rewind");
|
||||
@@ -339,10 +372,14 @@ async fn a_grouped_ballot_is_sealed_then_confirmed_by_the_indexer() {
|
||||
];
|
||||
assert_eq!(ld_engine::ballot::seal(doc_id, Some(ALICE), &lines, &salt).expect("seal").comment, comment);
|
||||
|
||||
// Nothing on the chain yet: pending, nothing counted.
|
||||
// Nothing on the chain yet: the intention is seen at once — her vote, the totals
|
||||
// shown — and waits for its proof; adoption does not count it (ADR 0012).
|
||||
assert_eq!(c.status(&b).await["status"], "pending");
|
||||
let doc = c.doc("monnaie-libre").await;
|
||||
assert_eq!(tally(&doc, &i1), (0, 0));
|
||||
assert_eq!((tally(&doc, &i1), pending(&doc, &i1)), ((1, 0), (1, 0)));
|
||||
assert_eq!((tally(&doc, &e3), pending(&doc, &e3)), ((0, 1), (0, 1)));
|
||||
assert_eq!(counted(&state, &doc, &i1).await, (0, 0));
|
||||
assert_eq!(mine(&doc, &i1), (Some("for"), Some("pending")));
|
||||
assert_eq!(find(&doc, &i1)["myBallot"]["status"], "pending");
|
||||
assert_eq!(find(&doc, &i1)["myBallot"]["choice"], "for");
|
||||
|
||||
@@ -354,10 +391,11 @@ async fn a_grouped_ballot_is_sealed_then_confirmed_by_the_indexer() {
|
||||
assert_eq!(s["voter"], "Alice");
|
||||
assert_eq!(s["block"], 3_400_000);
|
||||
let doc = c.doc("monnaie-libre").await;
|
||||
assert_eq!(tally(&doc, &i1), (1, 0));
|
||||
assert_eq!((tally(&doc, &i1), pending(&doc, &i1)), ((1, 0), (0, 0)));
|
||||
assert_eq!(tally(&doc, &e3), (0, 1));
|
||||
assert_eq!(counted(&state, &doc, &i1).await, (1, 0));
|
||||
// The session that asked sees « ton vote », confirmed on the chain.
|
||||
assert_eq!(find(&doc, &i1)["myVote"], "for");
|
||||
assert_eq!(mine(&doc, &i1), (Some("for"), Some("recorded")));
|
||||
assert_eq!(find(&doc, &i1)["myVoteChain"], true);
|
||||
assert!(find(&doc, &i1)["myBallot"].is_null());
|
||||
assert_eq!(find(&doc, &e3)["myVote"], "against");
|
||||
@@ -466,6 +504,17 @@ async fn a_signer_who_is_not_a_member_is_refused_and_told() {
|
||||
.await
|
||||
.expect("n");
|
||||
assert_eq!(n, 0, "no identity is created for a refused ballot");
|
||||
// And in her messages, once each (ADR 0012).
|
||||
let mut told = ballot_messages(&mut c).await;
|
||||
told.sort();
|
||||
let mut expected = vec![
|
||||
("ballot_refused".to_owned(), id_of(&former).to_owned()),
|
||||
("ballot_refused".to_owned(), id_of(&nobody).to_owned()),
|
||||
];
|
||||
expected.sort();
|
||||
assert_eq!(told, expected);
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
assert_eq!(ballot_messages(&mut c).await.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -491,7 +540,10 @@ async fn a_wrong_amount_or_recipient_is_refused() {
|
||||
assert_eq!((s["status"].as_str(), s["reason"].as_str()), (Some("refused"), Some("transfer")), "{s}");
|
||||
}
|
||||
assert_eq!(c.status(&gecko).await["status"], "pending");
|
||||
assert_eq!(tally(&c.doc("monnaie-libre").await, &i1), (0, 0));
|
||||
// The refused ones are set aside; the Ğecko code still waits: its intention shows, uncounted.
|
||||
let doc = c.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&doc, &i1), pending(&doc, &i1)), ((1, 0), (1, 0)));
|
||||
assert_eq!(counted(&state, &doc, &i1).await, (0, 0));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -722,8 +774,21 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
|
||||
let e1 = version(&doc, "E1", "inForce");
|
||||
let keys: Vec<String> = (1..=50u8).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() {
|
||||
let b = c.ballot(&doc, &[(&e4, Some("for")), (&e5, Some("for"))], None).await;
|
||||
// The fiftieth voice comes from another session: its intention is seen at once.
|
||||
let asker = if n == 49 { &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.
|
||||
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));
|
||||
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());
|
||||
}
|
||||
let index = 10_000 + u32::try_from(n).expect("n");
|
||||
indexer.pay(
|
||||
1_000 + index,
|
||||
@@ -764,16 +829,29 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
|
||||
}
|
||||
}
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
let s = c.status(&last).await;
|
||||
let s = luna.status(&last).await;
|
||||
assert_eq!(s["status"], "confirmed");
|
||||
assert_eq!(s["entered"][0]["code"], "E4", "{s}");
|
||||
let d = c.doc("monnaie-libre").await;
|
||||
assert_eq!(unit(&d, "E4")["inForce"]["id"], e4.as_str());
|
||||
// A ballot sealed before the objection counts, as a vote already cast: kept, but
|
||||
// the version held back cannot enter into force.
|
||||
assert_eq!(tally(&d, &e5), (50, 0));
|
||||
// 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!(find(&d, &e5)["votable"], false);
|
||||
assert!(unit(&d, "E5")["inForce"].is_null(), "an objection holds it back");
|
||||
let outcomes: Vec<(String, Option<String>)> =
|
||||
sqlx::query_as("SELECT proposal_id, outcome FROM ballot_lines WHERE ballot_id = ? ORDER BY proposal_id = ?")
|
||||
.bind(id_of(&last))
|
||||
.bind(&e5)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.expect("lines");
|
||||
assert_eq!(outcomes, [(e4.clone(), Some("counted".into())), (e5.clone(), Some("objection".into()))]);
|
||||
assert_eq!(ballot_messages(&mut luna).await, [("ballot_objection".to_owned(), id_of(&last).to_owned())]);
|
||||
let seen = luna.doc("monnaie-libre").await;
|
||||
let held = &find(&seen, &e5)["myBallot"];
|
||||
assert_eq!((held["status"].as_str(), held["reason"].as_str()), (Some("refused"), Some("objection")));
|
||||
assert_eq!(mine(&seen, &e4), (Some("for"), Some("recorded")));
|
||||
}
|
||||
|
||||
/// Yvv, 2026-11-01: « Retirer ou "contre" ». On the chain too, a held version takes a
|
||||
@@ -1050,7 +1128,10 @@ async fn a_comment_counts_once_its_block_is_final() {
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
assert_eq!(db_status(&state, id_of(&b)).await.0, "pending");
|
||||
assert_eq!(db_status(&state, id_of(&r)).await.0, "pending");
|
||||
assert_eq!(tally(&c.doc("monnaie-libre").await, &i1), (0, 0));
|
||||
// Her latest intention shows; nothing is counted before a final block.
|
||||
let doc = c.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&doc, &i1), pending(&doc, &i1)), ((1, 0), (1, 0)));
|
||||
assert_eq!(counted(&state, &doc, &i1).await, (0, 0));
|
||||
indexer.finalized(Some(100));
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
assert_eq!(db_status(&state, id_of(&b)).await.0, "confirmed");
|
||||
@@ -1287,15 +1368,17 @@ async fn fifty_voices_on_the_chain_bring_an_order_and_a_title_into_force() {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn without_a_vote_account_the_chain_vote_is_off_and_the_test_group_still_votes() {
|
||||
async fn without_a_vote_account_monnaie_libre_votes_in_simulation_and_the_test_group_still_votes() {
|
||||
let (mut c, _) = Client::new(Config::for_tests()).await;
|
||||
c.login("Chiara").await;
|
||||
let doc = c.doc("monnaie-libre").await;
|
||||
assert_eq!(doc["vote"]["way"], "chain");
|
||||
assert_eq!(doc["vote"]["open"], false);
|
||||
assert_eq!(doc["vote"]["reason"], "chain_off");
|
||||
// The chain closed: the vote is simulated, written by the server (ADR 0012).
|
||||
assert_eq!(doc["vote"]["way"], "server");
|
||||
assert_eq!(doc["vote"]["open"], true);
|
||||
assert_eq!(doc["vote"]["simulated"], true);
|
||||
assert!(doc["vote"]["reason"].is_null());
|
||||
assert!(doc["vote"]["chain"].is_null());
|
||||
assert_eq!(doc["canVote"], false);
|
||||
assert_eq!(doc["canVote"], true);
|
||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||
let i1 = version(&doc, "I1", "inForce");
|
||||
let (s, v) = c
|
||||
@@ -1308,8 +1391,11 @@ async fn without_a_vote_account_the_chain_vote_is_off_and_the_test_group_still_v
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("chain_vote_off")));
|
||||
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");
|
||||
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")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1430,3 +1516,365 @@ async fn a_grouped_vote_in_a_managed_group_is_one_write() {
|
||||
assert_eq!(find(&doc, &e5)["myVote"], "against");
|
||||
assert_eq!(find(&doc, &e5)["myVoteChain"], false);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The buffer of the votes (ADR 0012): the intention at once, the proof of the
|
||||
// chain, the reconciliation; the simulation while the chain is closed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_simulated_vote_in_monnaie_libre_is_seen_at_once_in_my_vote_and_the_totals() {
|
||||
let (mut c, state) = Client::new(Config::for_tests()).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 (i1, e3, e4) = (version(&doc, "I1", "inForce"), 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)));
|
||||
let d = c.doc("monnaie-libre").await;
|
||||
assert_eq!(tally(&d, &i1), (1, 0));
|
||||
assert_eq!(pending(&d, &i1), (0, 0), "nothing waits for the chain: the simulated confirmer proved it");
|
||||
assert_eq!(pair(&find(&d, &i1)["tallySimulated"]), (1, 0));
|
||||
assert_eq!(mine(&d, &i1), (Some("for"), Some("simulated")));
|
||||
assert_eq!(find(&d, &i1)["myVoteChain"], false);
|
||||
// By default a simulated vote counts like a proven one, adoption included.
|
||||
assert_eq!(counted(&state, &d, &i1).await, (1, 0));
|
||||
// Through the same buffer: a sealed ballot, simulated, proven at block 0, for the session.
|
||||
let row: (String, String, Option<i64>, Option<String>, String) =
|
||||
sqlx::query_as("SELECT mode, status, block, voter_id, requested_by FROM ballots")
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.expect("ballot");
|
||||
assert_eq!((row.0.as_str(), row.1.as_str(), row.2), ("simulated", "confirmed", Some(0)));
|
||||
assert_eq!(row.3.as_deref(), Some(row.4.as_str()), "the voice of the session: there is no wallet");
|
||||
let source: String = sqlx::query_scalar("SELECT source FROM votes").fetch_one(&state.db).await.expect("vote");
|
||||
assert_eq!(source, "simulated");
|
||||
// A grouped vote: one ballot, every line; then a withdrawal.
|
||||
let lines = json!([{ "proposalId": e3, "choice": "against" }, { "proposalId": e4, "choice": "for" }]);
|
||||
let v = c.ok("POST", &format!("/api/docs/{doc_id}/votes"), Some(json!({ "lines": lines }))).await;
|
||||
assert_eq!((v["count"].as_u64(), v["simulated"].as_bool()), (Some(2), Some(true)));
|
||||
c.ok("POST", &format!("/api/proposals/{i1}/vote"), Some(json!({ "choice": null }))).await;
|
||||
let d = c.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&d, &i1), tally(&d, &e3), tally(&d, &e4)), ((0, 0), (0, 1), (1, 0)));
|
||||
assert_eq!(mine(&d, &i1), (None, None));
|
||||
assert_eq!(mine(&d, &e3), (Some("against"), Some("simulated")));
|
||||
// Someone else sees the totals, never the vote.
|
||||
let mut yvv = c.other();
|
||||
yvv.login("Yvv").await;
|
||||
let seen = yvv.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&seen, &e3), mine(&seen, &e3)), ((0, 1), (None, None)));
|
||||
// Her ballots, marked simulated, with a receipt.
|
||||
let history = c.ok("GET", "/api/ballots", None).await;
|
||||
let list = history["ballots"].as_array().expect("ballots");
|
||||
assert_eq!(list.len(), 3);
|
||||
assert!(
|
||||
list.iter().all(|b| b["mode"] == "simulated" && b["status"] == "confirmed" && b["block"].is_null()),
|
||||
"{history}"
|
||||
);
|
||||
let id = list[0]["id"].as_str().expect("id");
|
||||
let r = c.ok("GET", &format!("/api/ballots/{id}/receipt"), None).await;
|
||||
assert_eq!(r["mode"], "simulated");
|
||||
// No ballot to sign while the chain is closed.
|
||||
let one = json!({ "lines": [{ "proposalId": i1, "choice": "for" }] });
|
||||
let (s, v) = c.call("POST", &format!("/api/docs/{doc_id}/ballots"), Some(one)).await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("chain_vote_off")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shown_but_never_adopted_when_simulated_votes_are_switched_off_for_adoption() {
|
||||
// Yvv may choose « simulé = affiché, jamais compté pour l'adoption » (ADR 0012).
|
||||
for adopts in [true, false] {
|
||||
let mut cfg = Config::for_tests();
|
||||
cfg.simulated_adopts = adopts;
|
||||
let (mut a, state) = Client::new(cfg).await;
|
||||
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");
|
||||
let e4 = version(&doc, "E4", "0");
|
||||
let mut b = a.other();
|
||||
b.login("Bruno").await;
|
||||
a.ok("POST", &format!("/api/proposals/{e4}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||
let v = b.ok("POST", &format!("/api/proposals/{e4}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||
let entered = !v["entered"].as_array().expect("entered").is_empty();
|
||||
let d = a.doc("monnaie-libre").await;
|
||||
assert_eq!(pair(&find(&d, &e4)["tallySimulated"]), (2, 0), "shown either way");
|
||||
assert_eq!(counted(&state, &d, &e4).await, if adopts { (2, 0) } else { (0, 0) });
|
||||
assert_eq!(entered, adopts, "adopts = {adopts}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Yvv, 2026-11-02: « Il compte, puis s'efface ». Opening the chain sets nothing
|
||||
/// aside; the act « supprime » does, and examines again what the simulated votes
|
||||
/// brought into force, with the proven votes only (`simulation::discard`).
|
||||
#[tokio::test]
|
||||
async fn opening_the_chain_sets_nothing_aside_the_act_does_and_examines_again_what_entered() {
|
||||
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();
|
||||
// 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");
|
||||
let (e4, e5, e6) = (version(&doc, "E4", "0"), version(&doc, "E5", "0"), version(&doc, "E6", "0"));
|
||||
let seed_i1 = version(&doc, "I1", "inForce");
|
||||
let i1_unit = unit(&doc, "I1")["id"].as_str().expect("unit").to_owned();
|
||||
let rival = c
|
||||
.ok(
|
||||
"POST",
|
||||
&format!("/api/docs/{doc_id}/proposals"),
|
||||
Some(json!({ "unitId": i1_unit, "title": "Raison d'être", "body": "Un autre texte pour I1, voté en simulation.", "submit": true })),
|
||||
)
|
||||
.await["id"]
|
||||
.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;
|
||||
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.
|
||||
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 lines = json!([
|
||||
{ "proposalId": e4, "choice": "for" }, { "proposalId": e5, "choice": "for" },
|
||||
{ "proposalId": e6, "choice": "for" }, { "proposalId": rival, "choice": "for" },
|
||||
]);
|
||||
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;
|
||||
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}");
|
||||
}
|
||||
// The chain opens again: nothing is set aside by itself — the simulated votes still count, marked.
|
||||
c.ok("POST", "/api/dev/vote-chain", Some(json!({ "open": true }))).await;
|
||||
let d = luna.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&d, &e5), pair(&find(&d, &e5)["tallySimulated"])), ((2, 0), (1, 0)));
|
||||
assert_eq!(mine(&d, &e5), (Some("for"), Some("simulated")));
|
||||
let (s, v) = luna.call("POST", &format!("/api/proposals/{e4}/vote"), Some(json!({ "choice": null }))).await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("chain_only")));
|
||||
// A second proven voice on E6, after it entered.
|
||||
let b = c.ballot(&d, &[(&e6, Some("for"))], None).await;
|
||||
indexer.pay(200, comment_of(&b), Signer { account: OTHER, identity: Some((9004, "Eve", true)) });
|
||||
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));
|
||||
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();
|
||||
v
|
||||
};
|
||||
let expected_told: Vec<(String, bool, bool)> =
|
||||
[("E4", false, false), ("E5", false, false), ("E6", true, false), ("I1", false, false)]
|
||||
.iter()
|
||||
.map(|(c, h, r)| ((*c).to_owned(), *h, *r))
|
||||
.collect();
|
||||
assert_eq!(verdicts(&told.reexamined), expected_told);
|
||||
assert_eq!(unit(&c.doc("monnaie-libre").await, "E5")["inForce"]["id"], e5.as_str());
|
||||
// Done: the simulated votes are gone; what does not hold on the proven votes returns.
|
||||
let done = ld_server::simulation::discard(&state, true).await.expect("act");
|
||||
let expected_done: Vec<(String, bool, bool)> =
|
||||
[("E4", false, true), ("E5", false, true), ("E6", true, false), ("I1", false, true)]
|
||||
.iter()
|
||||
.map(|(c, h, r)| ((*c).to_owned(), *h, *r))
|
||||
.collect();
|
||||
assert_eq!(verdicts(&done.reexamined), expected_done);
|
||||
let d = luna.doc("monnaie-libre").await;
|
||||
assert!(unit(&d, "E4")["inForce"].is_null() && unit(&d, "E5")["inForce"].is_null());
|
||||
assert_eq!(unit(&d, "E6")["inForce"]["id"], e6.as_str(), "two proven voices: it stays");
|
||||
assert_eq!(unit(&d, "I1")["inForce"]["id"], seed_i1.as_str(), "the text it had displaced comes back");
|
||||
assert_eq!(find(&d, &rival)["status"], "open");
|
||||
assert_eq!((tally(&d, &e5), mine(&d, &e5)), ((1, 0), (None, None)));
|
||||
let left: (i64, i64) = sqlx::query_as(
|
||||
"SELECT (SELECT COUNT(*) FROM votes WHERE source = 'simulated'), (SELECT COUNT(*) FROM ballots WHERE mode = 'simulated')",
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.expect("left");
|
||||
assert_eq!(left, (0, 0));
|
||||
let 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");
|
||||
// Once is enough.
|
||||
let again = ld_server::simulation::discard(&state, true).await.expect("again");
|
||||
assert_eq!((again.ballots, again.reexamined.len()), (0, 0));
|
||||
// Never without the dev login.
|
||||
let mut cfg = chain_config(&indexer);
|
||||
cfg.dev_login = false;
|
||||
let (mut other, _) = Client::new(cfg).await;
|
||||
let (s, _) = other.call("POST", "/api/dev/vote-chain", Some(json!({ "open": false }))).await;
|
||||
assert_eq!(s, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_intention_never_confirmed_is_set_aside_and_told() {
|
||||
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 i1 = version(&doc, "I1", "inForce");
|
||||
// A vote proven first: « contre ».
|
||||
let first = c.ballot(&doc, &[(&i1, Some("against"))], None).await;
|
||||
indexer.pay(100, comment_of(&first), alice());
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
// Then « pour », never paid: it takes the place of « contre » in the totals shown, for a while.
|
||||
let b = 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)));
|
||||
assert_eq!(mine(&d, &i1), (Some("for"), Some("pending")));
|
||||
// Someone else sees the totals shown, intention included.
|
||||
let mut yvv = c.other();
|
||||
yvv.login("Yvv").await;
|
||||
assert_eq!(tally(&yvv.doc("monnaie-libre").await, &i1), (1, 0));
|
||||
// Its window passes: set aside, and said — under the article, and in her messages.
|
||||
age(&state, "expires_at", -5, id_of(&b)).await;
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
assert_eq!(db_status(&state, id_of(&b)).await.0, "expired");
|
||||
let d = c.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&d, &i1), pending(&d, &i1)), ((0, 1), (0, 0)));
|
||||
assert_eq!(mine(&d, &i1), (Some("against"), Some("recorded")), "the vote of before stays");
|
||||
assert_eq!(find(&d, &i1)["myBallot"]["status"], "expired");
|
||||
assert_eq!(ballot_messages(&mut c).await, [("ballot_expired".to_owned(), id_of(&b).to_owned())]);
|
||||
let m = c.ok("GET", "/api/messages", None).await;
|
||||
let message = &m["messages"][0];
|
||||
assert_eq!((message["done"].as_bool(), message["gestures"].as_array().map(Vec::len)), (Some(true), Some(0)));
|
||||
assert_eq!(message["ballot"]["lines"][0]["code"], "I1");
|
||||
assert_eq!(message["ballot"]["lines"][0]["choice"], "for");
|
||||
assert_eq!(m["unread"], 1);
|
||||
// A ballot the wallet never signed is let go at once: nothing to tell.
|
||||
let dropped = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||
assert_eq!(tally(&c.doc("monnaie-libre").await, &i1), (1, 0));
|
||||
c.ok("DELETE", &format!("/api/ballots/{}", id_of(&dropped)), None).await;
|
||||
assert_eq!(tally(&c.doc("monnaie-libre").await, &i1), (0, 1));
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
assert_eq!(ballot_messages(&mut c).await.len(), 1);
|
||||
// Purged a week later, its message goes with it.
|
||||
age(&state, "settled_at", -8 * 24 * 3600, id_of(&b)).await;
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
assert_eq!(ballot_messages(&mut c).await.len(), 0);
|
||||
assert_eq!(c.ok("GET", "/api/messages/unread", None).await["unread"], 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn two_votes_of_one_identity_from_two_sessions_the_chain_keeps_the_last() {
|
||||
let indexer = Indexer::start().await;
|
||||
let (mut chiara, state) = Client::new(chain_config(&indexer)).await;
|
||||
chiara.login("Chiara").await;
|
||||
let mut other = chiara.other();
|
||||
other.login("Yvv").await;
|
||||
let doc = chiara.doc("monnaie-libre").await;
|
||||
let i1 = version(&doc, "I1", "inForce");
|
||||
// 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;
|
||||
// Before the proofs, each intention counts for its session (ADR 0012, the cost).
|
||||
assert_eq!(tally(&chiara.doc("monnaie-libre").await, &i1), (1, 1));
|
||||
// On the chain, « pour » comes last; the indexer reads it first.
|
||||
indexer.pay(200, comment_of(&a), alice());
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
indexer.pay(100, comment_of(&b), alice());
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
let both = (db_status(&state, id_of(&a)).await.0, db_status(&state, id_of(&b)).await.0);
|
||||
assert_eq!(both, ("confirmed".into(), "confirmed".into()));
|
||||
// One voice: the last in the order of the chain.
|
||||
let d = chiara.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&d, &i1), pending(&d, &i1), counted(&state, &d, &i1).await), ((1, 0), (0, 0), (1, 0)));
|
||||
let outcomes: Vec<(String, Option<String>)> =
|
||||
sqlx::query_as("SELECT ballot_id, outcome FROM ballot_lines ORDER BY ballot_id = ?")
|
||||
.bind(id_of(&a))
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.expect("lines");
|
||||
assert_eq!(
|
||||
outcomes,
|
||||
[(id_of(&b).to_owned(), Some("superseded".into())), (id_of(&a).to_owned(), Some("counted".into()))]
|
||||
);
|
||||
let votes: Vec<(String, String)> =
|
||||
sqlx::query_as("SELECT identity_id, choice FROM votes").fetch_all(&state.db).await.expect("votes");
|
||||
assert_eq!(votes, [("idty:9001".to_owned(), "for".to_owned())]);
|
||||
// The session whose vote was overtaken sees no vote of its own; nothing to tell.
|
||||
let theirs = other.doc("monnaie-libre").await;
|
||||
assert_eq!(mine(&theirs, &i1), (None, None));
|
||||
assert!(ballot_messages(&mut other).await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_for_proven_while_an_objection_holds_the_version_is_set_aside() {
|
||||
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 (e1, e5) = (version(&doc, "E1", "inForce"), 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());
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
// « Pour », sealed: seen at once in place of « contre ».
|
||||
let b = c.ballot(&doc, &[(&e5, Some("for"))], None).await;
|
||||
let d = c.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&d, &e5), mine(&d, &e5)), ((1, 0), (Some("for"), Some("pending"))));
|
||||
// An objection holds E5 back: the *for* still waiting weighs nothing.
|
||||
let mut luna = c.other();
|
||||
luna.login("Luna").await;
|
||||
luna.ok(
|
||||
"POST",
|
||||
&format!("/api/proposals/{e5}/objections"),
|
||||
Some(json!({ "otherId": e1, "kind": "contredit", "reason": "Ces deux textes disent le contraire." })),
|
||||
)
|
||||
.await;
|
||||
let d = c.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&d, &e5), pending(&d, &e5)), ((0, 1), (0, 0)));
|
||||
// The chain proves it now: set aside; « contre » stays; she is told.
|
||||
indexer.pay(200, comment_of(&b), alice());
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
assert_eq!(db_status(&state, id_of(&b)).await.0, "confirmed");
|
||||
let d = c.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&d, &e5), counted(&state, &d, &e5).await), ((0, 1), (0, 1)));
|
||||
assert_eq!(mine(&d, &e5), (Some("against"), Some("recorded")));
|
||||
let held = &find(&d, &e5)["myBallot"];
|
||||
assert_eq!((held["status"].as_str(), held["reason"].as_str()), (Some("refused"), Some("objection")));
|
||||
assert_eq!(ballot_messages(&mut c).await, [("ballot_objection".to_owned(), id_of(&b).to_owned())]);
|
||||
// A withdrawal, proven while held, counts.
|
||||
let w = c.ballot(&doc, &[(&e5, None)], None).await;
|
||||
indexer.pay(300, comment_of(&w), alice());
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
let d = c.doc("monnaie-libre").await;
|
||||
assert_eq!((tally(&d, &e5), mine(&d, &e5)), ((0, 0), (None, None)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_waiting_intention_counts_as_my_vote_in_decisions() {
|
||||
let indexer = Indexer::start().await;
|
||||
let (mut c, _) = Client::new(chain_config(&indexer)).await;
|
||||
c.login("Chiara").await;
|
||||
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");
|
||||
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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user