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:
Yvv
2026-11-03 04:06:07 +01:00
co-authored by Claude Opus 5.5
parent 6228d3913a
commit e2445f6467
14 changed files with 185 additions and 312 deletions
+53 -24
View File
@@ -1,7 +1,9 @@
//! 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
//! 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::http::{Request, StatusCode, header};
@@ -90,6 +92,14 @@ fn level(k: u8) -> InertiaLevel {
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> {
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;
c.login("Chiara").await;
let doc = c.doc("monnaie-libre").await;
// 3 everywhere, 4 for the two structural commitments.
let raised: Vec<&str> = doc["units"]
// 3 everywhere, low for the three seed proposals (ADR 0011).
let other: Vec<&str> = doc["units"]
.as_array()
.expect("units")
.iter()
.filter(|u| u["inertia"] != 3)
.map(|u| u["code"].as_str().expect("code"))
.collect();
assert_eq!(raised, ["E1", "E2"]);
assert!(unit(&doc, "E1")["inertia"] == 4 && unit(&doc, "E2")["inertia"] == 4);
assert_eq!(other, ["I1", "E1", "E2"]);
assert!(["I1", "E1", "E2"].iter().all(|c| unit(&doc, c)["inertia"] == LOW));
// No vote yet: the reference, 2C/W = 100 voters out of 5745.
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.
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;
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"], "required"), [50.0; 5], "nothing passes under C");
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;
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")));
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}");
let p = v["id"].as_str().expect("id").to_owned();
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)));
// W(essai) = 2: both vote for, the article goes to level 1.
assert_eq!(
(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;
let v = b.vote(&p, "for").await;
assert_eq!(v["entered"][0]["kind"], "inertia");
let doc = a.doc("essai").await;
let e2u = unit(&doc, "E2");
assert_eq!(e2u["inertia"], 1);
assert_eq!(e2u["inertia"], 5);
let changes = e2u["inertiaChanges"].as_array().expect("changes");
assert_eq!(changes.len(), 1);
assert_eq!(
(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.
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
.expect("adoption");
let params: Value = serde_json::from_str(&params).expect("json");
let judged = Scale { electorate: 2, floor: 1.0, participation: 0.5 }.params(level(LOW));
assert_eq!(
(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!(
(params["participation"].as_f64(), params["participation_source"].as_str()),
(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]
@@ -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 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 {
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");
seed.as_object_mut().expect("object").remove("modules");
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
}
/// Started again with the seed production ran after 30/10 (E1 and E2 at 4).
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");
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()
}
/// 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]
async fn a_new_base_starts_with_e1_and_e2_at_level_4() {
let (_, state) = Client::new().await;
async fn a_new_base_starts_low_and_the_raise_of_30_10_decides_nothing() {
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!(
decided(&inertia_decisions(&state.db).await),
decided(&inertia_decisions(&born.db).await),
[
("essai", "E1", "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),
]
);
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]