forked from yvv/decision
WIP lot S : floor_amend retiré (le quorum le remplace), fixture de production sous tests/fixtures/seed-production-2026-11-02/, reprise des tests (api, inertia, ballots, objections, typos)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,18 +34,3 @@ CREATE TABLE seed_proposal_amendments (
|
|||||||
at TEXT NOT NULL,
|
at TEXT NOT NULL,
|
||||||
PRIMARY KEY (amendment_id, doc_id)
|
PRIMARY KEY (amendment_id, doc_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- A floor C changed in an existing document (src/floor_amend.rs), should Yvv
|
|
||||||
-- lower it to start (question asked on 2026-11-02; none listed yet): decided
|
|
||||||
-- once per document, only if the floor is still the one expected. Each decision
|
|
||||||
-- is kept here: applied, refused (and why), or present.
|
|
||||||
CREATE TABLE floor_amendments (
|
|
||||||
amendment_id TEXT NOT NULL,
|
|
||||||
doc_id TEXT NOT NULL REFERENCES documents(id),
|
|
||||||
from_floor REAL, -- the floor found
|
|
||||||
to_floor REAL NOT NULL,
|
|
||||||
outcome TEXT NOT NULL, -- applied | refused | present
|
|
||||||
reason TEXT, -- why it was refused
|
|
||||||
at TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (amendment_id, doc_id)
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ pub struct Publish {
|
|||||||
pub author_name: String,
|
pub author_name: String,
|
||||||
pub author_email: String,
|
pub author_email: String,
|
||||||
/// Publish a document even when none of its articles is in force
|
/// Publish a document even when none of its articles is in force
|
||||||
/// (`LD_PUBLISH_WITHOUT_ARTICLES`). Off by default (orch, 2026-11-02, while Yvv
|
/// (`LD_PUBLISH_WITHOUT_ARTICLES`). Off by default — Yvv, 2026-11-02: « Suspendre
|
||||||
/// decides): the queue waits, and the first text that enters sends it (ADR 0011).
|
/// jusqu'au 1er texte »: the queue waits, and the first text that enters sends it
|
||||||
|
/// (ADR 0011).
|
||||||
pub without_articles: bool,
|
pub without_articles: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
//! Floor amendments — the floor C of an existing document changed (ADR 0011).
|
|
||||||
//!
|
|
||||||
//! C is the minimal number of *for* votes below which nothing passes (ADR 0002):
|
|
||||||
//! 50 in Monnaie libre, whatever the level of inertia. A low level for the seed
|
|
||||||
//! proposals does not make a start with five people possible; whether C is
|
|
||||||
//! lowered to start is Yvv's question (orch, 2026-11-02). **None is listed**:
|
|
||||||
//! the floor stays. Should he lower it, one entry here does it, with the seed's
|
|
||||||
//! `voteFloor` of the group set to the same value (new bases read the seed):
|
|
||||||
//!
|
|
||||||
//! - decided once per document, at the first start that knows the amendment;
|
|
||||||
//! - only if the floor is still the one expected (`from`); already `to`: present;
|
|
||||||
//! - kept in `floor_amendments` and in the logs; where the document publishes,
|
|
||||||
//! the publication queue gets a row (`document.json` carries `voteFloor`).
|
|
||||||
//!
|
|
||||||
//! Every threshold of the document follows at the next pass of the sweeper, and
|
|
||||||
//! so does the reference participation (2C/W) while fewer than five proposals
|
|
||||||
//! are voted (ADR 0007).
|
|
||||||
|
|
||||||
use crate::AppState;
|
|
||||||
use crate::seed::SeedDoc;
|
|
||||||
use crate::util;
|
|
||||||
|
|
||||||
/// One floor to change, in the document a seed instantiated in a group.
|
|
||||||
pub struct FloorAmendment {
|
|
||||||
/// Stable identifier: the decision is kept under it.
|
|
||||||
pub id: &'static str,
|
|
||||||
/// Slug of the seed document.
|
|
||||||
pub doc: &'static str,
|
|
||||||
/// The group whose document it changes.
|
|
||||||
pub group: &'static str,
|
|
||||||
/// The floor it must still hold.
|
|
||||||
pub from: f64,
|
|
||||||
/// The floor it gets: the seed's today for the group, which must say so.
|
|
||||||
pub to: f64,
|
|
||||||
/// Reason given to the publication queue, and so to the texts repository.
|
|
||||||
pub reason: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// None while Yvv has not decided (2026-11-02).
|
|
||||||
pub const AMENDMENTS: &[FloorAmendment] = &[];
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Verdict {
|
|
||||||
Apply,
|
|
||||||
/// The floor is already there: nothing to do.
|
|
||||||
Present,
|
|
||||||
Refuse(&'static str),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn same(a: f64, b: f64) -> bool {
|
|
||||||
(a - b).abs() < 1e-9
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The rule, apart from the database.
|
|
||||||
pub fn verdict(found: Option<f64>, a: &FloorAmendment) -> Verdict {
|
|
||||||
match found {
|
|
||||||
None => Verdict::Refuse("no_document"),
|
|
||||||
Some(c) if same(c, a.to) => Verdict::Present,
|
|
||||||
Some(c) if !same(c, a.from) => Verdict::Refuse("floor_differs"),
|
|
||||||
Some(_) => Verdict::Apply,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A decision taken at this start.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Outcome {
|
|
||||||
pub amendment: &'static str,
|
|
||||||
pub verdict: Verdict,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decides every amendment not yet decided.
|
|
||||||
pub async fn apply(state: &AppState, seeds: &[SeedDoc], list: &[FloorAmendment]) -> anyhow::Result<Vec<Outcome>> {
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for a in list {
|
|
||||||
if !seed_says(seeds, a) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let doc: Option<(String, f64, Option<String>)> =
|
|
||||||
sqlx::query_as("SELECT id, vote_floor, publish_path FROM documents WHERE slug = ? AND group_slug = ?")
|
|
||||||
.bind(a.doc)
|
|
||||||
.bind(a.group)
|
|
||||||
.fetch_optional(&state.db)
|
|
||||||
.await?;
|
|
||||||
let mut tx = state.db.begin().await?;
|
|
||||||
let doc_id = doc.as_ref().map(|(id, _, _)| id.clone());
|
|
||||||
if let Some(id) = &doc_id {
|
|
||||||
let decided: i64 =
|
|
||||||
sqlx::query_scalar("SELECT COUNT(*) FROM floor_amendments WHERE amendment_id = ? AND doc_id = ?")
|
|
||||||
.bind(a.id)
|
|
||||||
.bind(id)
|
|
||||||
.fetch_one(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
if decided > 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut v = verdict(doc.as_ref().map(|(_, c, _)| *c), a);
|
|
||||||
let now = util::now();
|
|
||||||
if let (Verdict::Apply, Some((id, c, publish_path))) = (v, &doc) {
|
|
||||||
// Written only over the floor that was judged.
|
|
||||||
let done = sqlx::query("UPDATE documents SET vote_floor = ? WHERE id = ? AND vote_floor = ?")
|
|
||||||
.bind(a.to)
|
|
||||||
.bind(id)
|
|
||||||
.bind(c)
|
|
||||||
.execute(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
if done.rows_affected() != 1 {
|
|
||||||
v = Verdict::Refuse("changed");
|
|
||||||
} else if publish_path.is_some() {
|
|
||||||
sqlx::query("INSERT INTO outbox (doc_id, reason, created_at, next_at) VALUES (?, ?, ?, ?)")
|
|
||||||
.bind(id)
|
|
||||||
.bind(a.reason)
|
|
||||||
.bind(&now)
|
|
||||||
.bind(&now)
|
|
||||||
.execute(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some(id) = doc_id else {
|
|
||||||
tracing::warn!(amendment = a.id, group = a.group, "floor amendment: no such document, nothing decided");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let (outcome, reason) = match v {
|
|
||||||
Verdict::Apply => ("applied", None),
|
|
||||||
Verdict::Present => ("present", None),
|
|
||||||
Verdict::Refuse(why) => ("refused", Some(why)),
|
|
||||||
};
|
|
||||||
sqlx::query(
|
|
||||||
"INSERT INTO floor_amendments (amendment_id, doc_id, from_floor, to_floor, outcome, reason, at)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
)
|
|
||||||
.bind(a.id)
|
|
||||||
.bind(&id)
|
|
||||||
.bind(doc.as_ref().map(|(_, c, _)| *c))
|
|
||||||
.bind(a.to)
|
|
||||||
.bind(outcome)
|
|
||||||
.bind(reason)
|
|
||||||
.bind(&now)
|
|
||||||
.execute(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
tx.commit().await?;
|
|
||||||
match v {
|
|
||||||
Verdict::Refuse(why) => {
|
|
||||||
tracing::warn!(amendment = a.id, group = a.group, reason = why, "floor amendment refused: the floor stays")
|
|
||||||
}
|
|
||||||
_ => tracing::info!(amendment = a.id, group = a.group, from = a.from, to = a.to, outcome, "floor amendment"),
|
|
||||||
}
|
|
||||||
if v == Verdict::Apply {
|
|
||||||
state.publish_wake.notify_one();
|
|
||||||
}
|
|
||||||
out.push(Outcome { amendment: a.id, verdict: v });
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The seed must give the group this floor: new bases read it there.
|
|
||||||
fn seed_says(seeds: &[SeedDoc], a: &FloorAmendment) -> bool {
|
|
||||||
let floor = seeds
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.slug == a.doc)
|
|
||||||
.and_then(|s| s.instances.iter().find(|i| i.group == a.group))
|
|
||||||
.map(|i| i.vote_floor);
|
|
||||||
if floor.is_some_and(|c| same(c, a.to)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
tracing::error!(
|
|
||||||
amendment = a.id,
|
|
||||||
group = a.group,
|
|
||||||
"floor amendment: the seed does not give this floor, nothing decided"
|
|
||||||
);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
const A: FloorAmendment = FloorAmendment {
|
|
||||||
id: "test",
|
|
||||||
doc: "doc",
|
|
||||||
group: "monnaie-libre",
|
|
||||||
from: 50.0,
|
|
||||||
to: 10.0,
|
|
||||||
reason: "plancher C abaissé : 50 → 10",
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn none_is_listed_while_yvv_decides() {
|
|
||||||
assert!(AMENDMENTS.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn the_floor_changes_only_from_the_one_expected() {
|
|
||||||
assert_eq!(verdict(Some(50.0), &A), Verdict::Apply);
|
|
||||||
assert_eq!(verdict(Some(10.0), &A), Verdict::Present);
|
|
||||||
assert_eq!(verdict(Some(40.0), &A), Verdict::Refuse("floor_differs"));
|
|
||||||
assert_eq!(verdict(None, &A), Verdict::Refuse("no_document"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@ pub mod deadlines;
|
|||||||
pub mod doc_title;
|
pub mod doc_title;
|
||||||
pub mod docs;
|
pub mod docs;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod floor_amend;
|
|
||||||
pub mod indexer;
|
pub mod indexer;
|
||||||
pub mod inertia_amend;
|
pub mod inertia_amend;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ pub async fn pass(state: &AppState) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
/// Does the document wait for a text before it is published? Yes when none of its
|
/// Does the document wait for a text before it is published? Yes when none of its
|
||||||
/// articles is in force — the seed proposals are not (ADR 0011) — unless the
|
/// articles is in force — the seed proposals are not (ADR 0011) — unless the
|
||||||
/// publication is told to go out anyway (`LD_PUBLISH_WITHOUT_ARTICLES`). The default
|
/// publication is told to go out anyway (`LD_PUBLISH_WITHOUT_ARTICLES`). The default is
|
||||||
/// is the orch's (2026-11-02), while Yvv decides: never a document without a text.
|
/// Yvv's (2026-11-02, « Suspendre jusqu'au 1er texte »): never a document without a text.
|
||||||
pub async fn waits_for_a_text(state: &AppState, doc_id: &str) -> anyhow::Result<bool> {
|
pub async fn waits_for_a_text(state: &AppState, doc_id: &str) -> anyhow::Result<bool> {
|
||||||
if state.cfg.publish.as_ref().is_none_or(|p| p.without_articles) {
|
if state.cfg.publish.as_ref().is_none_or(|p| p.without_articles) {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ use std::path::Path;
|
|||||||
|
|
||||||
/// The level of inertia a seed proposal starts at (ADR 0011). Yvv, 2026-11-02: « on
|
/// The level of inertia a seed proposal starts at (ADR 0011). Yvv, 2026-11-02: « on
|
||||||
/// pouvait l'amorcer avec une inertie basse, pour que le nombre de votes initiaux soit
|
/// pouvait l'amorcer avec une inertie basse, pour que le nombre de votes initiaux soit
|
||||||
/// assez petit pour commencer l'expérience en mode prod » — level 2 (« basse »), or
|
/// assez petit pour commencer l'expérience en mode prod » — level 2 (« basse »),
|
||||||
/// level 1 (« très basse »): his answer awaited, 2 by default. The one place to change:
|
/// confirmed by Yvv the same day (level 1, « très basse », was the other choice). The
|
||||||
|
/// one place to change:
|
||||||
/// the seed gives no level to an article it puts forward, and the seed proposal
|
/// the seed gives no level to an article it puts forward, and the seed proposal
|
||||||
/// amendment (`seed_proposal_amend`) writes this one.
|
/// amendment (`seed_proposal_amend`) writes this one.
|
||||||
pub const SEED_PROPOSAL_LEVEL: u8 = 2;
|
pub const SEED_PROPOSAL_LEVEL: u8 = 2;
|
||||||
@@ -133,7 +134,6 @@ pub async fn ensure(state: &AppState) -> anyhow::Result<()> {
|
|||||||
module_amend::apply(state, &seeds, module_amend::AMENDMENTS).await?;
|
module_amend::apply(state, &seeds, module_amend::AMENDMENTS).await?;
|
||||||
// After the text amendments: a starting text replaced first, then put forward.
|
// After the text amendments: a starting text replaced first, then put forward.
|
||||||
seed_proposal_amend::apply(state, &seeds, seed_proposal_amend::AMENDMENTS).await?;
|
seed_proposal_amend::apply(state, &seeds, seed_proposal_amend::AMENDMENTS).await?;
|
||||||
crate::floor_amend::apply(state, &seeds, crate::floor_amend::AMENDMENTS).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -491,13 +491,25 @@ mod tests {
|
|||||||
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
||||||
seed::ensure(&state).await.expect("seed");
|
seed::ensure(&state).await.expect("seed");
|
||||||
let now = "2026-10-31T00:00:00Z";
|
let now = "2026-10-31T00:00:00Z";
|
||||||
|
// A seed proposal (ADR 0011) the vote has just put in force.
|
||||||
let (id, doc, revision, title, body): (String, String, i64, String, String) = sqlx::query_as(
|
let (id, doc, revision, title, body): (String, String, i64, String, String) = sqlx::query_as(
|
||||||
"SELECT p.id, u.doc_id, p.revision, p.title, p.body FROM proposals p JOIN units u ON u.id = p.unit_id
|
"SELECT p.id, u.doc_id, p.revision, p.title, p.body FROM proposals p JOIN units u ON u.id = p.unit_id
|
||||||
WHERE p.status = 'in_force' AND p.kind = 'text' LIMIT 1",
|
WHERE p.status = 'open' AND p.seed_featured = 1 AND p.kind = 'text' LIMIT 1",
|
||||||
)
|
)
|
||||||
.fetch_one(&state.db)
|
.fetch_one(&state.db)
|
||||||
.await
|
.await
|
||||||
.expect("a version in force");
|
.expect("a seed proposal");
|
||||||
|
sqlx::query("UPDATE proposals SET status = 'in_force', entered_at = ? WHERE id = ?")
|
||||||
|
.bind(now)
|
||||||
|
.bind(&id)
|
||||||
|
.execute(&state.db)
|
||||||
|
.await
|
||||||
|
.expect("in force");
|
||||||
|
sqlx::query("UPDATE units SET in_force_id = ?1 WHERE id = (SELECT unit_id FROM proposals WHERE id = ?1)")
|
||||||
|
.bind(&id)
|
||||||
|
.execute(&state.db)
|
||||||
|
.await
|
||||||
|
.expect("unit");
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO identities (id, kind, display_name, created_at) VALUES ('dev:aude', 'dev', 'Aude', ?)",
|
"INSERT INTO identities (id, kind, display_name, created_at) VALUES ('dev:aude', 'dev', 'Aude', ?)",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ use ld_server::{AppState, CSRF_HEADER, CSRF_VALUE, Config, app, open_db, seed};
|
|||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
/// The seed production runs on 2026-11-02 (3f10a0e): texts in force, seeds a minima.
|
||||||
|
fn production_seed_dir() -> std::path::PathBuf {
|
||||||
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-production-2026-11-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn production_config() -> Config {
|
||||||
|
let mut cfg = Config::for_tests();
|
||||||
|
cfg.seed_dir = production_seed_dir();
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
|
||||||
struct Client {
|
struct Client {
|
||||||
router: axum::Router,
|
router: axum::Router,
|
||||||
cookie: Option<String>,
|
cookie: Option<String>,
|
||||||
@@ -16,8 +27,19 @@ struct Client {
|
|||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
async fn new() -> (Self, AppState) {
|
async fn new() -> (Self, AppState) {
|
||||||
|
Self::seeded(Config::for_tests()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A base seeded as production is before its seed proposal amendment (ADR 0011):
|
||||||
|
/// I1, E1 and E2 in force, seeds a minima — as they stay wherever it is refused.
|
||||||
|
/// For the scenarios that need texts in force; the seed of today has none.
|
||||||
|
async fn production() -> (Self, AppState) {
|
||||||
|
Self::seeded(production_config()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn seeded(cfg: Config) -> (Self, AppState) {
|
||||||
let db = open_db("sqlite::memory:").await.expect("db");
|
let db = open_db("sqlite::memory:").await.expect("db");
|
||||||
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
let state = AppState::new(cfg, db).await.expect("state");
|
||||||
seed::ensure(&state).await.expect("seed");
|
seed::ensure(&state).await.expect("seed");
|
||||||
(Self { router: app(state.clone()), cookie: None, set_cookie: None }, state)
|
(Self { router: app(state.clone()), cookie: None, set_cookie: None }, state)
|
||||||
}
|
}
|
||||||
@@ -88,7 +110,7 @@ async fn anonymous_sees_nothing_and_csrf_is_enforced() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn the_seed_a_minima_and_the_open_proposals() {
|
async fn the_seed_proposals_and_the_open_proposals() {
|
||||||
let (mut c, _) = Client::new().await;
|
let (mut c, _) = Client::new().await;
|
||||||
c.login("Chiara", false).await;
|
c.login("Chiara", false).await;
|
||||||
let (_, groups) = c.call("GET", "/api/groups", None).await;
|
let (_, groups) = c.call("GET", "/api/groups", None).await;
|
||||||
@@ -99,16 +121,10 @@ async fn the_seed_a_minima_and_the_open_proposals() {
|
|||||||
let id = doc_id(&mut c, "monnaie-libre").await;
|
let id = doc_id(&mut c, "monnaie-libre").await;
|
||||||
let (s, doc) = c.call("GET", &format!("/api/docs/{id}"), None).await;
|
let (s, doc) = c.call("GET", &format!("/api/docs/{id}"), None).await;
|
||||||
assert_eq!(s, StatusCode::OK);
|
assert_eq!(s, StatusCode::OK);
|
||||||
let in_force: Vec<&str> = doc["units"]
|
// No article in force: I1, E1 and E2 are seed proposals (ADR 0011), open, put forward.
|
||||||
.as_array()
|
assert!(doc["units"].as_array().expect("units").iter().all(|u| u["inForce"].is_null()));
|
||||||
.expect("units")
|
let e1 = &unit(&doc, "E1")["proposals"][0];
|
||||||
.iter()
|
assert_eq!((e1["seedProposal"].as_bool(), e1["status"].as_str()), (Some(true), Some("open")));
|
||||||
.filter(|u| !u["inForce"].is_null())
|
|
||||||
.filter_map(|u| u["code"].as_str())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(in_force, ["I1", "E1", "E2"]);
|
|
||||||
let e1 = &unit(&doc, "E1")["inForce"];
|
|
||||||
assert_eq!(e1["seedMinimal"], true);
|
|
||||||
assert_eq!(e1["tally"]["for"], 0);
|
assert_eq!(e1["tally"]["for"], 0);
|
||||||
assert_eq!(doc["doc"]["voteFloor"], 50.0);
|
assert_eq!(doc["doc"]["voteFloor"], 50.0);
|
||||||
assert_eq!(doc["doc"]["electorate"], 5745);
|
assert_eq!(doc["doc"]["electorate"], 5745);
|
||||||
@@ -119,8 +135,11 @@ async fn the_seed_a_minima_and_the_open_proposals() {
|
|||||||
let labels: Vec<&str> = e3["proposals"].as_array().expect("p").iter().filter_map(|p| p["label"].as_str()).collect();
|
let labels: Vec<&str> = e3["proposals"].as_array().expect("p").iter().filter_map(|p| p["label"].as_str()).collect();
|
||||||
assert_eq!(labels, ["1.0.0", "0.4.0-draft"], "E3 and its 0.4.0 variant");
|
assert_eq!(labels, ["1.0.0", "0.4.0-draft"], "E3 and its 0.4.0 variant");
|
||||||
assert_eq!(e3["inertia"], 3);
|
assert_eq!(e3["inertia"], 3);
|
||||||
// The two structural commitments start one level up (Yvv, 2026-10-30).
|
// The seed proposals start low (Yvv, 2026-11-02).
|
||||||
assert_eq!((unit(&doc, "E1")["inertia"].as_u64(), unit(&doc, "E2")["inertia"].as_u64()), (Some(4), Some(4)));
|
let low = u64::from(ld_server::seed::SEED_PROPOSAL_LEVEL);
|
||||||
|
for code in ["I1", "E1", "E2"] {
|
||||||
|
assert_eq!(unit(&doc, code)["inertia"].as_u64(), Some(low), "{code}");
|
||||||
|
}
|
||||||
// Translations seeded.
|
// Translations seeded.
|
||||||
assert!(e1["translations"]["en"]["body"].as_str().expect("en").contains("commit"));
|
assert!(e1["translations"]["en"]["body"].as_str().expect("en").contains("commit"));
|
||||||
}
|
}
|
||||||
@@ -155,7 +174,7 @@ async fn adoption_in_the_test_group_and_publication_queued_only_where_published(
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_rival_replaces_the_seed_and_the_seed_returns_among_proposals() {
|
async fn a_rival_replaces_the_seed_and_the_seed_returns_among_proposals() {
|
||||||
let (mut a, _) = Client::new().await;
|
let (mut a, _) = Client::production().await;
|
||||||
a.login("Yvv", false).await;
|
a.login("Yvv", false).await;
|
||||||
let mut b = a.with(None);
|
let mut b = a.with(None);
|
||||||
b.login("Chiara", false).await;
|
b.login("Chiara", false).await;
|
||||||
@@ -185,7 +204,7 @@ async fn a_rival_replaces_the_seed_and_the_seed_returns_among_proposals() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn editing_is_not_voting() {
|
async fn editing_is_not_voting() {
|
||||||
let (mut a, _) = Client::new().await;
|
let (mut a, _) = Client::production().await;
|
||||||
// An administrator of trustinWallet answers for the starting texts (no author).
|
// An administrator of trustinWallet answers for the starting texts (no author).
|
||||||
a.login("Yvv", true).await;
|
a.login("Yvv", true).await;
|
||||||
let mut b = a.with(None);
|
let mut b = a.with(None);
|
||||||
@@ -343,7 +362,7 @@ async fn editing_is_not_voting() {
|
|||||||
/// the full cycle: tests/objections.rs).
|
/// the full cycle: tests/objections.rs).
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn an_objection_holds_back_the_version_that_is_not_in_force() {
|
async fn an_objection_holds_back_the_version_that_is_not_in_force() {
|
||||||
let (mut a, _) = Client::new().await;
|
let (mut a, _) = Client::production().await;
|
||||||
a.login("Yvv", false).await;
|
a.login("Yvv", false).await;
|
||||||
let mut b = a.with(None);
|
let mut b = a.with(None);
|
||||||
b.login("Chiara", false).await;
|
b.login("Chiara", false).await;
|
||||||
@@ -389,7 +408,8 @@ async fn inertia_change_is_voted_at_the_level_of_the_article() {
|
|||||||
let (_, again) = a.call("POST", &format!("/api/units/{uid}/inertia"), Some(json!({ "target": "max" }))).await;
|
let (_, again) = a.call("POST", &format!("/api/units/{uid}/inertia"), Some(json!({ "target": "max" }))).await;
|
||||||
assert_eq!(again["id"], p.as_str());
|
assert_eq!(again["id"], p.as_str());
|
||||||
let (_, d) = a.call("GET", &format!("/api/proposals/{p}"), None).await;
|
let (_, d) = a.call("GET", &format!("/api/proposals/{p}"), None).await;
|
||||||
assert_eq!((d["proposal"]["inertiaTarget"].as_u64(), d["proposal"]["votedAtInertia"].as_u64()), (Some(5), Some(4)));
|
let low = u64::from(ld_server::seed::SEED_PROPOSAL_LEVEL);
|
||||||
|
assert_eq!((d["proposal"]["inertiaTarget"].as_u64(), d["proposal"]["votedAtInertia"].as_u64()), (Some(5), Some(low)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -400,7 +420,7 @@ async fn previews_and_assistant() {
|
|||||||
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await;
|
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await;
|
||||||
assert_eq!(v["units"].as_array().expect("u").len(), 33);
|
assert_eq!(v["units"].as_array().expect("u").len(), 33);
|
||||||
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=wellrated"), None).await;
|
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=wellrated"), None).await;
|
||||||
assert_eq!(v["units"].as_array().expect("u").len(), 3, "nothing is well rated at zero votes");
|
assert_eq!(v["units"].as_array().expect("u").len(), 0, "nothing in force, nothing well rated at zero votes");
|
||||||
let (_, v) = a
|
let (_, v) = a
|
||||||
.call("POST", &format!("/api/docs/{id}/assist"), Some(json!({
|
.call("POST", &format!("/api/docs/{id}/assist"), Some(json!({
|
||||||
"text": "Je m'engage sur l'honneur à n'avoir jamais qu'un seul compte cocréateur de monnaie G1.\n\nJe m'engage à rester joignable par mes certifieurs.",
|
"text": "Je m'engage sur l'honneur à n'avoir jamais qu'un seul compte cocréateur de monnaie G1.\n\nJe m'engage à rester joignable par mes certifieurs.",
|
||||||
@@ -741,7 +761,7 @@ async fn publication_writes_the_texts_in_force_to_the_repository() {
|
|||||||
std::fs::create_dir_all(&dir).expect("dir");
|
std::fs::create_dir_all(&dir).expect("dir");
|
||||||
let remote = dir.join("textes.git");
|
let remote = dir.join("textes.git");
|
||||||
assert!(Command::new("git").args(["init", "-q", "--bare"]).arg(&remote).status().expect("git").success());
|
assert!(Command::new("git").args(["init", "-q", "--bare"]).arg(&remote).status().expect("git").success());
|
||||||
let mut cfg = Config::for_tests();
|
let mut cfg = production_config();
|
||||||
cfg.data_dir = dir.join("data");
|
cfg.data_dir = dir.join("data");
|
||||||
cfg.publish = Some(ld_server::config::Publish {
|
cfg.publish = Some(ld_server::config::Publish {
|
||||||
remote: remote.display().to_string(),
|
remote: remote.display().to_string(),
|
||||||
@@ -815,7 +835,7 @@ async fn a_refused_first_push_goes_out_again_and_a_new_empty_repository_receives
|
|||||||
path
|
path
|
||||||
};
|
};
|
||||||
let config = |remote: &std::path::Path| {
|
let config = |remote: &std::path::Path| {
|
||||||
let mut cfg = Config::for_tests();
|
let mut cfg = production_config();
|
||||||
cfg.data_dir = dir.join("data");
|
cfg.data_dir = dir.join("data");
|
||||||
cfg.publish = Some(ld_server::config::Publish {
|
cfg.publish = Some(ld_server::config::Publish {
|
||||||
remote: remote.display().to_string(),
|
remote: remote.display().to_string(),
|
||||||
@@ -906,12 +926,18 @@ fn today_seed() -> Value {
|
|||||||
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
|
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The seed production runs, I1 in force: the amendment of 30/10 is replayed on it.
|
||||||
|
fn production_seed() -> Value {
|
||||||
|
let path = production_seed_dir().join("acte-engagement-certification.json");
|
||||||
|
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
|
||||||
|
}
|
||||||
|
|
||||||
/// The seed as it was before 2026-10-30: the Préambule in force.
|
/// The seed as it was before 2026-10-30: the Préambule in force.
|
||||||
fn first_seed_dir(tag: &str) -> std::path::PathBuf {
|
fn first_seed_dir(tag: &str) -> std::path::PathBuf {
|
||||||
let dir = std::env::temp_dir().join(format!("ld-first-seed-{tag}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("ld-first-seed-{tag}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
std::fs::create_dir_all(&dir).expect("dir");
|
std::fs::create_dir_all(&dir).expect("dir");
|
||||||
let mut seed = today_seed();
|
let mut seed = production_seed();
|
||||||
let i1 = seed["units"].as_array_mut().expect("units").iter_mut().find(|u| u["code"] == "I1").expect("I1");
|
let i1 = seed["units"].as_array_mut().expect("units").iter_mut().find(|u| u["code"] == "I1").expect("I1");
|
||||||
for (lang, title, body) in PREAMBULE {
|
for (lang, title, body) in PREAMBULE {
|
||||||
i1["versions"][0]["title"][lang] = json!(title);
|
i1["versions"][0]["title"][lang] = json!(title);
|
||||||
@@ -931,8 +957,10 @@ async fn base_with_the_preambule(tag: &str, mut cfg: Config) -> (Client, AppStat
|
|||||||
(Client { router: app(state.clone()), cookie: None, set_cookie: None }, state)
|
(Client { router: app(state.clone()), cookie: None, set_cookie: None }, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same base, started again with today's seed (and its amendments).
|
/// The same base, started again with the seed of production (and its amendments): the
|
||||||
async fn restart(before: &AppState, cfg: Config) -> (Client, AppState) {
|
/// seed of today then puts I1 forward (tests/seed_proposals.rs).
|
||||||
|
async fn restart(before: &AppState, mut cfg: Config) -> (Client, AppState) {
|
||||||
|
cfg.seed_dir = production_seed_dir();
|
||||||
let state = AppState::new(cfg, before.db.clone()).await.expect("state");
|
let state = AppState::new(cfg, before.db.clone()).await.expect("state");
|
||||||
seed::ensure(&state).await.expect("seed");
|
seed::ensure(&state).await.expect("seed");
|
||||||
(Client { router: app(state.clone()), cookie: None, set_cookie: None }, state)
|
(Client { router: app(state.clone()), cookie: None, set_cookie: None }, state)
|
||||||
@@ -953,10 +981,12 @@ fn outcomes(rows: &[(String, String, Option<String>, String)]) -> Vec<(&str, &st
|
|||||||
rows.iter().map(|(g, o, r, _)| (g.as_str(), o.as_str(), r.as_deref())).collect()
|
rows.iter().map(|(g, o, r, _)| (g.as_str(), o.as_str(), r.as_deref())).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// I1 as it reads: its text in force, else its seed proposal (ADR 0011).
|
||||||
async fn i1_of(c: &mut Client, group: &str) -> Value {
|
async fn i1_of(c: &mut Client, group: &str) -> Value {
|
||||||
let id = doc_id(c, group).await;
|
let id = doc_id(c, group).await;
|
||||||
let (_, doc) = c.call("GET", &format!("/api/docs/{id}"), None).await;
|
let (_, doc) = c.call("GET", &format!("/api/docs/{id}"), None).await;
|
||||||
unit(&doc, "I1")["inForce"].clone()
|
let u = unit(&doc, "I1");
|
||||||
|
if u["inForce"].is_null() { u["proposals"][0].clone() } else { u["inForce"].clone() }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -967,13 +997,13 @@ async fn a_new_base_starts_with_the_raison_d_etre() {
|
|||||||
let i1 = i1_of(&mut c, group).await;
|
let i1 = i1_of(&mut c, group).await;
|
||||||
assert_eq!(i1["title"], "Raison d'être");
|
assert_eq!(i1["title"], "Raison d'être");
|
||||||
assert!(i1["body"].as_str().expect("body").starts_with(RAISON_D_ETRE), "{i1}");
|
assert!(i1["body"].as_str().expect("body").starts_with(RAISON_D_ETRE), "{i1}");
|
||||||
assert_eq!((i1["revision"].as_i64(), i1["seedMinimal"].as_bool()), (Some(1), Some(true)));
|
assert_eq!((i1["revision"].as_i64(), i1["seedProposal"].as_bool()), (Some(1), Some(true)));
|
||||||
for (lang, title) in [("en", "Purpose"), ("es", "Razón de ser")] {
|
for (lang, title) in [("en", "Purpose"), ("es", "Razón de ser")] {
|
||||||
let tr = &i1["translations"][lang];
|
let tr = &i1["translations"][lang];
|
||||||
assert_eq!((tr["title"].as_str(), tr["stale"].as_bool()), (Some(title), Some(false)), "{lang}");
|
assert_eq!((tr["title"].as_str(), tr["stale"].as_bool()), (Some(title), Some(false)), "{lang}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Born with the text: nothing to replace, and it is said once.
|
// Born with the text, put forward: nothing to replace, and it is said once.
|
||||||
assert_eq!(outcomes(&decisions(&state).await), [("essai", "present", None), ("monnaie-libre", "present", None)]);
|
assert_eq!(outcomes(&decisions(&state).await), [("essai", "present", None), ("monnaie-libre", "present", None)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,7 +1016,8 @@ async fn the_shipped_amendment_goes_from_the_preambule_to_the_seed_text() {
|
|||||||
let seed = today_seed();
|
let seed = today_seed();
|
||||||
let i1 = seed["units"].as_array().expect("units").iter().find(|u| u["code"] == "I1").expect("I1");
|
let i1 = seed["units"].as_array().expect("units").iter().find(|u| u["code"] == "I1").expect("I1");
|
||||||
let v = &i1["versions"][0];
|
let v = &i1["versions"][0];
|
||||||
assert_eq!(v["status"], "in_force");
|
// Today the starting text is a seed proposal (ADR 0011); the text is the same.
|
||||||
|
assert_eq!((v["status"].as_str(), v["seedProposal"].as_bool()), (Some("open"), Some(true)));
|
||||||
let (title, body) = (v["title"]["fr"].as_str().expect("title"), v["body"]["fr"].as_str().expect("body"));
|
let (title, body) = (v["title"]["fr"].as_str().expect("title"), v["body"]["fr"].as_str().expect("body"));
|
||||||
assert_eq!(ld_server::seed_amend::fingerprint(title, body), a.to, "the text of the seed today");
|
assert_eq!(ld_server::seed_amend::fingerprint(title, body), a.to, "the text of the seed today");
|
||||||
}
|
}
|
||||||
@@ -999,7 +1030,7 @@ async fn a_seed_amendment_replaces_an_untouched_preambule_once_and_republishes_i
|
|||||||
std::fs::create_dir_all(&dir).expect("dir");
|
std::fs::create_dir_all(&dir).expect("dir");
|
||||||
let remote = dir.join("textes.git");
|
let remote = dir.join("textes.git");
|
||||||
assert!(Command::new("git").args(["init", "-q", "--bare"]).arg(&remote).status().expect("git").success());
|
assert!(Command::new("git").args(["init", "-q", "--bare"]).arg(&remote).status().expect("git").success());
|
||||||
let mut cfg = Config::for_tests();
|
let mut cfg = production_config();
|
||||||
cfg.data_dir = dir.join("data");
|
cfg.data_dir = dir.join("data");
|
||||||
cfg.publish = Some(ld_server::config::Publish {
|
cfg.publish = Some(ld_server::config::Publish {
|
||||||
remote: remote.display().to_string(),
|
remote: remote.display().to_string(),
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ async fn a_grouped_ballot_is_sealed_then_confirmed_by_the_indexer() {
|
|||||||
assert_eq!(doc["vote"]["chain"]["amount"], 10);
|
assert_eq!(doc["vote"]["chain"]["amount"], 10);
|
||||||
assert_eq!(doc["vote"]["chain"]["test"], false);
|
assert_eq!(doc["vote"]["chain"]["test"], false);
|
||||||
|
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let e3 = version(&doc, "E3", "0");
|
let e3 = version(&doc, "E3", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for")), (&e3, Some("against"))], Some(ALICE_GENERIC)).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for")), (&e3, Some("against"))], Some(ALICE_GENERIC)).await;
|
||||||
let comment = b["comment"].as_str().expect("comment");
|
let comment = b["comment"].as_str().expect("comment");
|
||||||
@@ -416,7 +416,7 @@ async fn a_silent_indexer_keeps_the_ballot_pending() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
indexer.pay(100, b["comment"].as_str().expect("comment"), alice());
|
indexer.pay(100, b["comment"].as_str().expect("comment"), alice());
|
||||||
indexer.silent(true);
|
indexer.silent(true);
|
||||||
@@ -442,7 +442,7 @@ async fn a_signer_who_is_not_a_member_is_refused_and_told() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let former = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let former = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
indexer.pay(
|
indexer.pay(
|
||||||
100,
|
100,
|
||||||
@@ -474,7 +474,7 @@ async fn a_wrong_amount_or_recipient_is_refused() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
// Bound to the wallet that signs: its own transaction without the transfer is refused, and told.
|
// Bound to the wallet that signs: its own transaction without the transfer is refused, and told.
|
||||||
let short = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
let short = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
||||||
indexer.add(100, 1, short["comment"].as_str().expect("c"), alice(), &[(VOTE_ACCOUNT, 9)]);
|
indexer.add(100, 1, short["comment"].as_str().expect("c"), alice(), &[(VOTE_ACCOUNT, 9)]);
|
||||||
@@ -500,7 +500,7 @@ async fn confirming_twice_changes_nothing() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
indexer.pay(100, b["comment"].as_str().expect("c"), alice());
|
indexer.pay(100, b["comment"].as_str().expect("c"), alice());
|
||||||
// The page and the background task at once.
|
// The page and the background task at once.
|
||||||
@@ -527,8 +527,8 @@ async fn the_last_one_stands_in_the_order_of_the_chain() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let e1 = version(&doc, "E1", "inForce");
|
let e1 = version(&doc, "E1", "0");
|
||||||
// Three ballots of the same person, signed in this order on the chain: for (I1, E1), against (I1), none (E1).
|
// Three ballots of the same person, signed in this order on the chain: for (I1, E1), against (I1), none (E1).
|
||||||
let first = c.ballot(&doc, &[(&i1, Some("for")), (&e1, Some("for"))], None).await;
|
let first = c.ballot(&doc, &[(&i1, Some("for")), (&e1, Some("for"))], None).await;
|
||||||
let second = c.ballot(&doc, &[(&i1, Some("against"))], None).await;
|
let second = c.ballot(&doc, &[(&i1, Some("against"))], None).await;
|
||||||
@@ -609,7 +609,7 @@ async fn a_bound_ballot_ignores_a_copy_of_its_remark_by_another_account() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
||||||
let comment = b["comment"].as_str().expect("c");
|
let comment = b["comment"].as_str().expect("c");
|
||||||
// Charlie copies the remark seen in the pool and gets it in first.
|
// Charlie copies the remark seen in the pool and gets it in first.
|
||||||
@@ -631,7 +631,7 @@ async fn a_ballot_not_seen_in_time_is_let_go() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
let expires = b["expiresAt"].as_str().expect("expires");
|
let expires = b["expiresAt"].as_str().expect("expires");
|
||||||
assert!(
|
assert!(
|
||||||
@@ -660,7 +660,7 @@ async fn a_ballot_the_wallet_refused_to_sign_is_let_go_at_once() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let first = c.ballot(&doc, &[(&i1, Some("against"))], Some(ALICE)).await;
|
let first = c.ballot(&doc, &[(&i1, Some("against"))], Some(ALICE)).await;
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
||||||
assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["id"], b["id"]);
|
assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["id"], b["id"]);
|
||||||
@@ -693,7 +693,7 @@ async fn a_session_sees_its_own_ballots_only() {
|
|||||||
let mut other = chiara.other();
|
let mut other = chiara.other();
|
||||||
other.login("Yvv").await;
|
other.login("Yvv").await;
|
||||||
let doc = chiara.doc("monnaie-libre").await;
|
let doc = chiara.doc("monnaie-libre").await;
|
||||||
let (i1, e1) = (version(&doc, "I1", "inForce"), version(&doc, "E1", "inForce"));
|
let (i1, e1) = (version(&doc, "I1", "0"), version(&doc, "E1", "0"));
|
||||||
// Both sign with the same wallet (one person, two sessions): two ballots, two versions.
|
// Both sign with the same wallet (one person, two sessions): two ballots, two versions.
|
||||||
let a = chiara.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let a = chiara.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
let b = other.ballot(&doc, &[(&e1, Some("against"))], None).await;
|
let b = other.ballot(&doc, &[(&e1, Some("against"))], None).await;
|
||||||
@@ -719,7 +719,7 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
|
|||||||
let e4 = version(&doc, "E4", "0");
|
let e4 = version(&doc, "E4", "0");
|
||||||
let e5 = version(&doc, "E5", "0");
|
let e5 = version(&doc, "E5", "0");
|
||||||
let reason = "Ces deux textes disent le contraire l'un de l'autre.";
|
let reason = "Ces deux textes disent le contraire l'un de l'autre.";
|
||||||
let e1 = version(&doc, "E1", "inForce");
|
let e1 = version(&doc, "E1", "0");
|
||||||
let keys: Vec<String> = (1..=50u8).map(|n| address::encode(G1_PREFIX, &[n; 32])).collect();
|
let keys: Vec<String> = (1..=50u8).map(|n| address::encode(G1_PREFIX, &[n; 32])).collect();
|
||||||
let mut last = Value::Null;
|
let mut last = Value::Null;
|
||||||
for (n, account) in keys.iter().enumerate() {
|
for (n, account) in keys.iter().enumerate() {
|
||||||
@@ -732,7 +732,7 @@ async fn fifty_voices_on_the_chain_make_a_text_pass_and_an_objection_holds_it()
|
|||||||
);
|
);
|
||||||
if n == 49 {
|
if n == 49 {
|
||||||
// The last ballot is signed; before the chain confirms it, an objection holds
|
// The last ballot is signed; before the chain confirms it, an objection holds
|
||||||
// E5 back against the text in force E1 (ADR 0010): no new ballot may hold it.
|
// E5 back against the seed proposal E1, written before it (ADR 0010, 0011): no new ballot may hold it.
|
||||||
let o = c
|
let o = c
|
||||||
.ok(
|
.ok(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -785,7 +785,7 @@ async fn on_the_chain_a_held_version_takes_against_and_withdrawal_never_for() {
|
|||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
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 (e1, e4, e5) = (version(&doc, "E1", "0"), version(&doc, "E4", "0"), version(&doc, "E5", "0"));
|
||||||
let o = c
|
let o = c
|
||||||
.ok(
|
.ok(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -856,7 +856,7 @@ async fn a_long_author_is_refused_at_once() {
|
|||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
// The body may hold 256 KiB: an author of 250 000 characters passes it.
|
// The body may hold 256 KiB: an author of 250 000 characters passes it.
|
||||||
for n in [80_000usize, 250_000] {
|
for n in [80_000usize, 250_000] {
|
||||||
let body = json!({ "lines": [{ "proposalId": i1, "choice": "for" }], "author": "z".repeat(n) });
|
let body = json!({ "lines": [{ "proposalId": i1, "choice": "for" }], "author": "z".repeat(n) });
|
||||||
@@ -875,7 +875,7 @@ async fn the_background_task_reaches_every_pending_ballot() {
|
|||||||
let (mut a, state) = Client::new(chain_config(&indexer)).await;
|
let (mut a, state) = Client::new(chain_config(&indexer)).await;
|
||||||
a.login("Anna").await;
|
a.login("Anna").await;
|
||||||
let doc = a.doc("monnaie-libre").await;
|
let doc = a.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let mut b = a.other();
|
let mut b = a.other();
|
||||||
b.login("Bruno").await;
|
b.login("Bruno").await;
|
||||||
let mut c = a.other();
|
let mut c = a.other();
|
||||||
@@ -910,7 +910,7 @@ async fn letting_ballots_go_does_not_lift_the_cap() {
|
|||||||
a.login("Anna").await;
|
a.login("Anna").await;
|
||||||
let doc = a.doc("monnaie-libre").await;
|
let doc = a.doc("monnaie-libre").await;
|
||||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
let b = a.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let b = a.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
a.ok("DELETE", &format!("/api/ballots/{}", id_of(&b)), None).await;
|
a.ok("DELETE", &format!("/api/ballots/{}", id_of(&b)), None).await;
|
||||||
@@ -965,7 +965,7 @@ async fn an_unbound_ballot_is_not_settled_by_an_unpaid_copy() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
indexer.add(99, 1, comment_of(&b), Signer { account: OTHER, identity: None }, &[]);
|
indexer.add(99, 1, comment_of(&b), Signer { account: OTHER, identity: None }, &[]);
|
||||||
indexer.add(
|
indexer.add(
|
||||||
@@ -992,7 +992,7 @@ async fn an_unbound_ballot_can_be_taken_by_a_paid_copy() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
indexer.pay(99, comment_of(&b), Signer { account: CHARLIE, identity: Some((7, "Charlie", true)) });
|
indexer.pay(99, comment_of(&b), Signer { account: CHARLIE, identity: Some((7, "Charlie", true)) });
|
||||||
indexer.pay(100, comment_of(&b), alice());
|
indexer.pay(100, comment_of(&b), alice());
|
||||||
@@ -1012,7 +1012,7 @@ async fn an_older_pending_ballot_never_hides_a_newer_confirmed_vote() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
// A Ğecko code shown, never paid; then a vote from the wallet, confirmed.
|
// A Ğecko code shown, never paid; then a vote from the wallet, confirmed.
|
||||||
c.ballot(&doc, &[(&i1, Some("against"))], None).await;
|
c.ballot(&doc, &[(&i1, Some("against"))], None).await;
|
||||||
let newer = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
let newer = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
||||||
@@ -1035,7 +1035,7 @@ async fn a_comment_counts_once_its_block_is_final() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
||||||
let r = c.ballot(&doc, &[(&i1, Some("for"))], Some(BOB)).await;
|
let r = c.ballot(&doc, &[(&i1, Some("for"))], Some(BOB)).await;
|
||||||
indexer.finalized(Some(99));
|
indexer.finalized(Some(99));
|
||||||
@@ -1066,7 +1066,7 @@ async fn copies_never_hide_the_real_comment() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
||||||
for n in 0..120 {
|
for n in 0..120 {
|
||||||
indexer.pay(100 + n, comment_of(&b), Signer { account: CHARLIE, identity: Some((7, "Charlie", true)) });
|
indexer.pay(100 + n, comment_of(&b), Signer { account: CHARLIE, identity: Some((7, "Charlie", true)) });
|
||||||
@@ -1099,7 +1099,7 @@ async fn a_gecko_ballot_is_settled_by_the_background_task_even_at_its_end() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
age(&state, "expires_at", -5, id_of(&b)).await;
|
age(&state, "expires_at", -5, id_of(&b)).await;
|
||||||
indexer.pay(100, comment_of(&b), alice());
|
indexer.pay(100, comment_of(&b), alice());
|
||||||
@@ -1124,7 +1124,7 @@ async fn the_receipt_is_for_the_one_who_asked_and_proves_the_vote() {
|
|||||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let (i1, e3) = (version(&doc, "I1", "inForce"), version(&doc, "E3", "0"));
|
let (i1, e3) = (version(&doc, "I1", "0"), version(&doc, "E3", "0"));
|
||||||
let b = c.ballot(&doc, &[(&i1, Some("for")), (&e3, Some("against"))], Some(ALICE_GENERIC)).await;
|
let b = c.ballot(&doc, &[(&i1, Some("for")), (&e3, Some("against"))], Some(ALICE_GENERIC)).await;
|
||||||
indexer.pay(100, comment_of(&b), alice());
|
indexer.pay(100, comment_of(&b), alice());
|
||||||
ballots::sweep(&state).await.expect("sweep");
|
ballots::sweep(&state).await.expect("sweep");
|
||||||
@@ -1183,7 +1183,7 @@ async fn a_name_known_only_from_a_chain_vote_stays_free() {
|
|||||||
let (mut admin, state) = Client::new(chain_config(&indexer)).await;
|
let (mut admin, state) = Client::new(chain_config(&indexer)).await;
|
||||||
admin.login_admin("Yvv").await;
|
admin.login_admin("Yvv").await;
|
||||||
let doc = admin.doc("monnaie-libre").await;
|
let doc = admin.doc("monnaie-libre").await;
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
for (block, signer) in [(100, alice()), (101, Signer { account: CHARLIE, identity: Some((9003, "Charlie", true)) })]
|
for (block, signer) in [(100, alice()), (101, Signer { account: CHARLIE, identity: Some((9003, "Charlie", true)) })]
|
||||||
{
|
{
|
||||||
let b = admin.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
let b = admin.ballot(&doc, &[(&i1, Some("for"))], None).await;
|
||||||
@@ -1297,7 +1297,7 @@ async fn without_a_vote_account_the_chain_vote_is_off_and_the_test_group_still_v
|
|||||||
assert!(doc["vote"]["chain"].is_null());
|
assert!(doc["vote"]["chain"].is_null());
|
||||||
assert_eq!(doc["canVote"], false);
|
assert_eq!(doc["canVote"], false);
|
||||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let (s, v) = c
|
let (s, v) = c
|
||||||
.call(
|
.call(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -1308,7 +1308,7 @@ 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")));
|
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("chain_vote_off")));
|
||||||
let essai = c.doc("essai").await;
|
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"]["way"].as_str(), essai["vote"]["open"].as_bool()), (Some("server"), Some(true)));
|
||||||
let p = version(&essai, "I1", "inForce");
|
let p = version(&essai, "I1", "0");
|
||||||
c.ok("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
c.ok("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1353,9 +1353,9 @@ async fn what_a_ballot_may_not_hold() {
|
|||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||||
let i1 = version(&doc, "I1", "inForce");
|
let i1 = version(&doc, "I1", "0");
|
||||||
let essai = c.doc("essai").await;
|
let essai = c.doc("essai").await;
|
||||||
let elsewhere = version(&essai, "I1", "inForce");
|
let elsewhere = version(&essai, "I1", "0");
|
||||||
for (lines, author, code) in [
|
for (lines, author, code) in [
|
||||||
(json!([]), Value::Null, "lines"),
|
(json!([]), Value::Null, "lines"),
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
//! Inertia in five levels (ADR 0007): the levels and the scale served, the
|
//! Inertia in five levels (ADR 0007): the levels and the scale served, the
|
||||||
//! usual participation of a group, the change of inertia voted at the level
|
//! usual participation of a group, the change of inertia voted at the level
|
||||||
//! of its article, the migration of the four words of before, and the
|
//! of its article, the migration of the four words of before, and the
|
||||||
//! amendment that raises E1 and E2 to level 4 in an existing base.
|
//! amendment that raised E1 and E2 to level 4 in an existing base (30/10; since
|
||||||
|
//! 2026-11-02 the seed puts them forward at a low level, ADR 0011: the scenarios
|
||||||
|
//! of 30/10 are replayed on the seed production ran then).
|
||||||
|
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::http::{Request, StatusCode, header};
|
use axum::http::{Request, StatusCode, header};
|
||||||
@@ -90,6 +92,14 @@ fn level(k: u8) -> InertiaLevel {
|
|||||||
InertiaLevel::new(k).expect("1 to 5")
|
InertiaLevel::new(k).expect("1 to 5")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The level seed proposals start at (ADR 0011).
|
||||||
|
const LOW: u8 = ld_server::seed::SEED_PROPOSAL_LEVEL;
|
||||||
|
|
||||||
|
/// The seed production runs on 2026-11-02 (3f10a0e): I1, E1, E2 in force, E1 and E2 at 4.
|
||||||
|
fn production_seed_dir() -> std::path::PathBuf {
|
||||||
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-production-2026-11-02")
|
||||||
|
}
|
||||||
|
|
||||||
fn numbers(levels: &Value, key: &str) -> Vec<f64> {
|
fn numbers(levels: &Value, key: &str) -> Vec<f64> {
|
||||||
levels.as_array().expect("levels").iter().map(|l| l[key].as_f64().expect(key)).collect()
|
levels.as_array().expect("levels").iter().map(|l| l[key].as_f64().expect(key)).collect()
|
||||||
}
|
}
|
||||||
@@ -99,16 +109,16 @@ async fn the_five_levels_and_the_scale_are_served() {
|
|||||||
let (mut c, _) = Client::new().await;
|
let (mut c, _) = Client::new().await;
|
||||||
c.login("Chiara").await;
|
c.login("Chiara").await;
|
||||||
let doc = c.doc("monnaie-libre").await;
|
let doc = c.doc("monnaie-libre").await;
|
||||||
// 3 everywhere, 4 for the two structural commitments.
|
// 3 everywhere, low for the three seed proposals (ADR 0011).
|
||||||
let raised: Vec<&str> = doc["units"]
|
let other: Vec<&str> = doc["units"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.expect("units")
|
.expect("units")
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|u| u["inertia"] != 3)
|
.filter(|u| u["inertia"] != 3)
|
||||||
.map(|u| u["code"].as_str().expect("code"))
|
.map(|u| u["code"].as_str().expect("code"))
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(raised, ["E1", "E2"]);
|
assert_eq!(other, ["I1", "E1", "E2"]);
|
||||||
assert!(unit(&doc, "E1")["inertia"] == 4 && unit(&doc, "E2")["inertia"] == 4);
|
assert!(["I1", "E1", "E2"].iter().all(|c| unit(&doc, c)["inertia"] == LOW));
|
||||||
|
|
||||||
// No vote yet: the reference, 2C/W = 100 voters out of 5745.
|
// No vote yet: the reference, 2C/W = 100 voters out of 5745.
|
||||||
let p = &doc["doc"]["participation"];
|
let p = &doc["doc"]["participation"];
|
||||||
@@ -138,9 +148,9 @@ async fn the_five_levels_and_the_scale_are_served() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A version: the five levels at its own participation, and the level it is voted at.
|
// A version: the five levels at its own participation, and the level it is voted at.
|
||||||
let e1 = unit(&doc, "E1")["inForce"]["id"].as_str().expect("e1").to_owned();
|
let e1 = unit(&doc, "E1")["proposals"][0]["id"].as_str().expect("e1").to_owned();
|
||||||
let d = c.get(&format!("/api/proposals/{e1}")).await;
|
let d = c.get(&format!("/api/proposals/{e1}")).await;
|
||||||
assert_eq!(d["proposal"]["votedAtInertia"], 4);
|
assert_eq!(d["proposal"]["votedAtInertia"], LOW);
|
||||||
assert_eq!(numbers(&d["levels"], "cast"), [0.0; 5]);
|
assert_eq!(numbers(&d["levels"], "cast"), [0.0; 5]);
|
||||||
assert_eq!(numbers(&d["levels"], "required"), [50.0; 5], "nothing passes under C");
|
assert_eq!(numbers(&d["levels"], "required"), [50.0; 5], "nothing passes under C");
|
||||||
assert_eq!(d["participation"]["source"], "reference");
|
assert_eq!(d["participation"]["source"], "reference");
|
||||||
@@ -160,25 +170,28 @@ async fn a_change_of_inertia_is_voted_at_the_level_of_its_article_and_journaled(
|
|||||||
let (s, v) = a.call("POST", &format!("/api/units/{e2}/inertia"), Some(to(bad.clone()))).await;
|
let (s, v) = a.call("POST", &format!("/api/units/{e2}/inertia"), Some(to(bad.clone()))).await;
|
||||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("inertia")), "{bad}");
|
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("inertia")), "{bad}");
|
||||||
}
|
}
|
||||||
let (s, v) = a.call("POST", &format!("/api/units/{e2}/inertia"), Some(to(json!(4)))).await;
|
let (s, v) = a.call("POST", &format!("/api/units/{e2}/inertia"), Some(to(json!(LOW)))).await;
|
||||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("same_inertia")));
|
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("same_inertia")));
|
||||||
let (s, v) = a.call("POST", &format!("/api/units/{e2}/inertia"), Some(to(json!(1)))).await;
|
let (s, v) = a.call("POST", &format!("/api/units/{e2}/inertia"), Some(to(json!(5)))).await;
|
||||||
assert_eq!(s, StatusCode::OK, "{v}");
|
assert_eq!(s, StatusCode::OK, "{v}");
|
||||||
let p = v["id"].as_str().expect("id").to_owned();
|
let p = v["id"].as_str().expect("id").to_owned();
|
||||||
let d = a.get(&format!("/api/proposals/{p}")).await;
|
let d = a.get(&format!("/api/proposals/{p}")).await;
|
||||||
assert_eq!((d["proposal"]["inertiaTarget"].as_u64(), d["proposal"]["votedAtInertia"].as_u64()), (Some(1), Some(4)));
|
assert_eq!(
|
||||||
// W(essai) = 2: both vote for, the article goes to level 1.
|
(d["proposal"]["inertiaTarget"].as_u64(), d["proposal"]["votedAtInertia"].as_u64()),
|
||||||
|
(Some(5), Some(u64::from(LOW)))
|
||||||
|
);
|
||||||
|
// W(essai) = 2: both vote for, the article goes to level 5.
|
||||||
a.vote(&p, "for").await;
|
a.vote(&p, "for").await;
|
||||||
let v = b.vote(&p, "for").await;
|
let v = b.vote(&p, "for").await;
|
||||||
assert_eq!(v["entered"][0]["kind"], "inertia");
|
assert_eq!(v["entered"][0]["kind"], "inertia");
|
||||||
let doc = a.doc("essai").await;
|
let doc = a.doc("essai").await;
|
||||||
let e2u = unit(&doc, "E2");
|
let e2u = unit(&doc, "E2");
|
||||||
assert_eq!(e2u["inertia"], 1);
|
assert_eq!(e2u["inertia"], 5);
|
||||||
let changes = e2u["inertiaChanges"].as_array().expect("changes");
|
let changes = e2u["inertiaChanges"].as_array().expect("changes");
|
||||||
assert_eq!(changes.len(), 1);
|
assert_eq!(changes.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(changes[0]["from"].as_u64(), changes[0]["to"].as_u64(), changes[0]["cause"].as_str()),
|
(changes[0]["from"].as_u64(), changes[0]["to"].as_u64(), changes[0]["cause"].as_str()),
|
||||||
(Some(4), Some(1), Some("vote"))
|
(Some(u64::from(LOW)), Some(5), Some("vote"))
|
||||||
);
|
);
|
||||||
// The adoption keeps the level it was judged at, and the scale.
|
// The adoption keeps the level it was judged at, and the scale.
|
||||||
let params: String = sqlx::query_scalar("SELECT params FROM adoptions WHERE proposal_id = ?")
|
let params: String = sqlx::query_scalar("SELECT params FROM adoptions WHERE proposal_id = ?")
|
||||||
@@ -187,15 +200,16 @@ async fn a_change_of_inertia_is_voted_at_the_level_of_its_article_and_journaled(
|
|||||||
.await
|
.await
|
||||||
.expect("adoption");
|
.expect("adoption");
|
||||||
let params: Value = serde_json::from_str(¶ms).expect("json");
|
let params: Value = serde_json::from_str(¶ms).expect("json");
|
||||||
|
let judged = Scale { electorate: 2, floor: 1.0, participation: 0.5 }.params(level(LOW));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(params["level"].as_u64(), params["majority_pct"].as_f64(), params["share_pct"].as_f64()),
|
(params["level"].as_u64(), params["majority_pct"].as_f64(), params["share_pct"].as_f64()),
|
||||||
(Some(4), Some(60.0), Some(83.0))
|
(Some(u64::from(LOW)), Some(judged.majority_pct), Some(level(LOW).share_pct()))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(params["participation"].as_f64(), params["participation_source"].as_str()),
|
(params["participation"].as_f64(), params["participation_source"].as_str()),
|
||||||
(Some(0.5), Some("reference"))
|
(Some(0.5), Some("reference"))
|
||||||
);
|
);
|
||||||
assert!(params["gradient"].as_f64().is_some_and(|g| (g - gradient(level(4), 0.5)).abs() < 1e-12));
|
assert!(params["gradient"].as_f64().is_some_and(|g| (g - gradient(level(LOW), 0.5)).abs() < 1e-12));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -358,9 +372,9 @@ async fn the_usual_participation_counts_the_statuses_it_names_and_only_those() {
|
|||||||
// The base as production holds it on 2026-10-30, then the new version
|
// The base as production holds it on 2026-10-30, then the new version
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// The seed as it was then: no modules yet (ADR 0009 came after).
|
/// The seed as it was then: no modules yet (ADR 0009 came after), I1, E1 and E2 in force.
|
||||||
fn seed_of_the_time(tag: &str) -> std::path::PathBuf {
|
fn seed_of_the_time(tag: &str) -> std::path::PathBuf {
|
||||||
let today = Config::for_tests().seed_dir.join("acte-engagement-certification.json");
|
let today = production_seed_dir().join("acte-engagement-certification.json");
|
||||||
let mut seed: Value = serde_json::from_str(&std::fs::read_to_string(today).expect("seed")).expect("json");
|
let mut seed: Value = serde_json::from_str(&std::fs::read_to_string(today).expect("seed")).expect("json");
|
||||||
seed.as_object_mut().expect("object").remove("modules");
|
seed.as_object_mut().expect("object").remove("modules");
|
||||||
let dir = std::env::temp_dir().join(format!("ld-levels-seed-{tag}-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("ld-levels-seed-{tag}-{}", std::process::id()));
|
||||||
@@ -397,8 +411,11 @@ async fn start_new_version(pool: &SqlitePool) -> AppState {
|
|||||||
restart(pool).await
|
restart(pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Started again with the seed production ran after 30/10 (E1 and E2 at 4).
|
||||||
async fn restart(pool: &SqlitePool) -> AppState {
|
async fn restart(pool: &SqlitePool) -> AppState {
|
||||||
let state = AppState::new(Config::for_tests(), pool.clone()).await.expect("state");
|
let mut cfg = Config::for_tests();
|
||||||
|
cfg.seed_dir = production_seed_dir();
|
||||||
|
let state = AppState::new(cfg, pool.clone()).await.expect("state");
|
||||||
seed::ensure(&state).await.expect("seed");
|
seed::ensure(&state).await.expect("seed");
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -449,11 +466,26 @@ fn decided(rows: &[Decision]) -> Vec<(&str, &str, &str, Option<&str>)> {
|
|||||||
rows.iter().map(|(g, u, o, r)| (g.as_str(), u.as_str(), o.as_str(), r.as_deref())).collect()
|
rows.iter().map(|(g, u, o, r)| (g.as_str(), u.as_str(), o.as_str(), r.as_deref())).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Since 2026-11-02 the seed gives E1 and E2 the low level of seed proposals: the
|
||||||
|
/// raise of 30/10 is superseded, decided in production and nowhere else.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_new_base_starts_with_e1_and_e2_at_level_4() {
|
async fn a_new_base_starts_low_and_the_raise_of_30_10_decides_nothing() {
|
||||||
let (_, state) = Client::new().await;
|
let (mut c, state) = Client::new().await;
|
||||||
|
assert_eq!(decided(&inertia_decisions(&state.db).await), []);
|
||||||
|
let changes: i64 =
|
||||||
|
sqlx::query_scalar("SELECT COUNT(*) FROM inertia_changes").fetch_one(&state.db).await.expect("n");
|
||||||
|
assert_eq!(changes, 0, "born low: nothing changed");
|
||||||
|
c.login("Chiara").await;
|
||||||
|
let doc = c.doc("monnaie-libre").await;
|
||||||
|
assert!(["E1", "E2"].iter().all(|code| unit(&doc, code)["inertia"] == LOW));
|
||||||
|
// On the seed of production, the raise is still there to decide: present at birth.
|
||||||
|
let mut cfg = Config::for_tests();
|
||||||
|
cfg.seed_dir = production_seed_dir();
|
||||||
|
let db = open_db("sqlite::memory:").await.expect("db");
|
||||||
|
let born = AppState::new(cfg, db).await.expect("state");
|
||||||
|
seed::ensure(&born).await.expect("seed");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decided(&inertia_decisions(&state.db).await),
|
decided(&inertia_decisions(&born.db).await),
|
||||||
[
|
[
|
||||||
("essai", "E1", "present", None),
|
("essai", "E1", "present", None),
|
||||||
("essai", "E2", "present", None),
|
("essai", "E2", "present", None),
|
||||||
@@ -461,9 +493,6 @@ async fn a_new_base_starts_with_e1_and_e2_at_level_4() {
|
|||||||
("monnaie-libre", "E2", "present", None),
|
("monnaie-libre", "E2", "present", None),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
let changes: i64 =
|
|
||||||
sqlx::query_scalar("SELECT COUNT(*) FROM inertia_changes").fetch_one(&state.db).await.expect("n");
|
|
||||||
assert_eq!(changes, 0, "born at 4: nothing changed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -9,15 +9,25 @@ use ld_server::{AppState, CSRF_HEADER, CSRF_VALUE, Config, adoption, app, deadli
|
|||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
/// The seed of production before its seed proposal amendment (ADR 0011): texts in force.
|
||||||
|
fn production() -> Config {
|
||||||
|
let mut cfg = Config::for_tests();
|
||||||
|
cfg.seed_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-production-2026-11-02");
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
|
||||||
struct Client {
|
struct Client {
|
||||||
router: axum::Router,
|
router: axum::Router,
|
||||||
cookie: Option<String>,
|
cookie: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
|
/// A base seeded as production is before its seed proposal amendment (ADR 0011):
|
||||||
|
/// I1, E1 and E2 in force, seeds a minima — as they stay wherever it is refused.
|
||||||
|
/// These scenarios need texts in force; the seed of today has none.
|
||||||
async fn new() -> (Self, AppState) {
|
async fn new() -> (Self, AppState) {
|
||||||
let db = open_db("sqlite::memory:").await.expect("db");
|
let db = open_db("sqlite::memory:").await.expect("db");
|
||||||
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
let state = AppState::new(production(), db).await.expect("state");
|
||||||
seed::ensure(&state).await.expect("seed");
|
seed::ensure(&state).await.expect("seed");
|
||||||
(Self { router: app(state.clone()), cookie: None }, state)
|
(Self { router: app(state.clone()), cookie: None }, state)
|
||||||
}
|
}
|
||||||
@@ -670,7 +680,7 @@ async fn the_same_objection_sent_at_once_stands_once() {
|
|||||||
let path = std::env::temp_dir().join(format!("ld-lot-f-twice-{}.db", std::process::id()));
|
let path = std::env::temp_dir().join(format!("ld-lot-f-twice-{}.db", std::process::id()));
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
let db = open_db(&format!("sqlite://{}", path.display())).await.expect("db");
|
let db = open_db(&format!("sqlite://{}", path.display())).await.expect("db");
|
||||||
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
let state = AppState::new(production(), db).await.expect("state");
|
||||||
seed::ensure(&state).await.expect("seed");
|
seed::ensure(&state).await.expect("seed");
|
||||||
let mut chiara = Client { router: app(state.clone()), cookie: None };
|
let mut chiara = Client { router: app(state.clone()), cookie: None };
|
||||||
chiara.login("Chiara", false).await;
|
chiara.login("Chiara", false).await;
|
||||||
@@ -719,7 +729,7 @@ async fn a_deadline_passed_while_stopped_is_taken_at_start() {
|
|||||||
let url = format!("sqlite://{}", path.display());
|
let url = format!("sqlite://{}", path.display());
|
||||||
let (b, oid) = {
|
let (b, oid) = {
|
||||||
let db = open_db(&url).await.expect("db");
|
let db = open_db(&url).await.expect("db");
|
||||||
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
let state = AppState::new(production(), db).await.expect("state");
|
||||||
seed::ensure(&state).await.expect("seed");
|
seed::ensure(&state).await.expect("seed");
|
||||||
let mut chiara = Client { router: app(state.clone()), cookie: None };
|
let mut chiara = Client { router: app(state.clone()), cookie: None };
|
||||||
chiara.login("Chiara", false).await;
|
chiara.login("Chiara", false).await;
|
||||||
@@ -776,7 +786,7 @@ async fn contradiction_signals_of_before_become_contredits() {
|
|||||||
let mut before = sqlx::migrate!("./migrations");
|
let mut before = sqlx::migrate!("./migrations");
|
||||||
before.migrations = before.migrations.iter().filter(|m| m.version < 8).cloned().collect::<Vec<_>>().into();
|
before.migrations = before.migrations.iter().filter(|m| m.version < 8).cloned().collect::<Vec<_>>().into();
|
||||||
before.run(&pool).await.expect("up to 0007");
|
before.run(&pool).await.expect("up to 0007");
|
||||||
let state = AppState::new(Config::for_tests(), pool.clone()).await.expect("state");
|
let state = AppState::new(production(), pool.clone()).await.expect("state");
|
||||||
// The documents as the image of before wrote them (its amendments read tables of now).
|
// The documents as the image of before wrote them (its amendments read tables of now).
|
||||||
seed::documents(&state).await.expect("seed");
|
seed::documents(&state).await.expect("seed");
|
||||||
let id = |code: &'static str, which: &'static str| {
|
let id = |code: &'static str, which: &'static str| {
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ const PREAMBULE: [(&str, &str, &str); 3] = [
|
|||||||
/// The seed production runs on 2026-11-02 (3f10a0e): I1, E1 and E2 in force, seeds a
|
/// The seed production runs on 2026-11-02 (3f10a0e): I1, E1 and E2 in force, seeds a
|
||||||
/// minima, I1 and E1 at level 3 and 4.
|
/// minima, I1 and E1 at level 3 and 4.
|
||||||
fn production_seed() -> Value {
|
fn production_seed() -> Value {
|
||||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-production-2026-11-02.json");
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-production-2026-11-02/acte-engagement-certification.json");
|
||||||
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
|
serde_json::from_str(&std::fs::read_to_string(path).expect("seed")).expect("json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
|
/// A base seeded as production is before its seed proposal amendment (ADR 0011):
|
||||||
|
/// I1, E1 and E2 in force, seeds a minima — as they stay wherever it is refused.
|
||||||
|
/// These scenarios need texts in force; the seed of today has none.
|
||||||
async fn new() -> (Self, AppState) {
|
async fn new() -> (Self, AppState) {
|
||||||
let db = open_db("sqlite::memory:").await.expect("db");
|
let db = open_db("sqlite::memory:").await.expect("db");
|
||||||
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
let mut cfg = Config::for_tests();
|
||||||
|
cfg.seed_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-production-2026-11-02");
|
||||||
|
let state = AppState::new(cfg, db).await.expect("state");
|
||||||
seed::ensure(&state).await.expect("seed");
|
seed::ensure(&state).await.expect("seed");
|
||||||
(Self { router: app(state.clone()), cookie: None }, state)
|
(Self { router: app(state.clone()), cookie: None }, state)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user