Lot T4 : les tests d'avant suivent le seed de T4 sans rien perdre de ce qu'ils prouvent

Les tests qui lisaient F, N ou O rejouent leurs transitions sur le seed de M8
(tests/fixtures/seed-m8) : seed_quorum_texts, seed_rules, seed_proposals, api,
quorum ; ils disent en plus que ces amendements sont dépassés par le retrait
(superseded_by). Les comptes suivent le document de T4 : 27 articles, quatre
Propositions seed (I1, E1, E12, E2), 33 modules à voter (27 articles, le titre,
l'ordre de 5 sections). modules.rs : l'ordre sous vote se rejoue sur
« conclusion » (K1, K2) ; « un ordre d'un seul article n'a rien à décider » sur
une base née du seed de M8 (O1). seed_amend : tests unitaires de
Version::SeedProposal et de la ligne seed_proposal (put_forward). order.rs ne lit
que les colonnes d'avant 0012 : sections_now ne sert qu'à la naissance des
modules, et article_amend n'en garde que les articles vivants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 21:21:44 +01:00
co-authored by Claude Opus 5.5
parent dda1ca85dd
commit 093d1ca471
14 changed files with 384 additions and 189 deletions
+51 -35
View File
@@ -39,9 +39,17 @@
//! engine on one grid (`grid`): W from 100, medians under 1 % and above 50 % of W, every quorum an
//! author may set, the five levels — the closed list given to the reviewer is
//! `docs/chantiers/acte-certification/t3c-liste-fermee.md`.
//!
//! Lot T4 (2026-11-03, Yvv: « ordonnancement, inertie, et formule de vote sont des infos de
//! fonctionnement »): F1.1, F1.2 and N1.1 leave the document (ADR 0014); they are no texts to
//! vote any more. Their last texts — the seed of M8 (de1b7eb), kept in
//! `tests/fixtures/seed-m8/` — stay the reference of what the interface will say of the rules
//! (lot U3), and every truth this file checks of them against the engine still holds there. The
//! amendments that wrote them are replayed on that seed, as production played them.
use ld_engine::inertia::{InertiaLevel, Participation, Scale, gradient, quorum_bounds};
use ld_engine::threshold::{FormulaParams, Tally, judged_voters, passes, required_ratio, wot_threshold, yes_needed};
use ld_server::retire::RETIREMENT;
use ld_server::seed_amend::{self, AMENDMENTS, Version};
use ld_server::wot::PLAUSIBLE;
use ld_server::{AppState, Config, open_db, seed};
@@ -64,8 +72,15 @@ fn read(path: PathBuf) -> Value {
serde_json::from_str(&std::fs::read_to_string(path).expect("read")).expect("json")
}
fn today_seed() -> Value {
read(Config::for_tests().seed_dir.join("acte-engagement-certification.json"))
/// The seed production runs since M8 (de1b7eb): the last that holds F1.1, F1.2 and N1.1, and
/// the reference of their texts since lot T4 took them out of the document.
fn m8_seed() -> Value {
read(fixtures().join("seed-m8/acte-engagement-certification.json"))
}
/// A start with the seed of M8: lot T3 decides.
fn m8_config(tag: &str) -> Config {
config(seed_dir(&format!("{tag}-m8"), &m8_seed()))
}
fn fixtures() -> PathBuf {
@@ -89,7 +104,7 @@ fn m6b_texts() -> Value {
/// The seed production runs since M6b: today's, with the texts of lot T.
fn m6b_seed() -> Value {
with_texts(&today_seed(), &m6b_texts(), &CODES)
with_texts(&m8_seed(), &m6b_texts(), &CODES)
}
/// F1.1, F1.2 and N1.1 as lot T2 wrote them: production's since M6c (84f015a), unchanged by M7b.
@@ -99,7 +114,7 @@ fn t2_texts() -> Value {
/// The seed production runs since M6c: today's, with the texts of lot T2.
fn t2_seed() -> Value {
with_texts(&today_seed(), &t2_texts(), &CODES)
with_texts(&m8_seed(), &t2_texts(), &CODES)
}
fn unit<'a>(seed: &'a Value, code: &str) -> &'a Value {
@@ -481,7 +496,7 @@ fn formulas(code: &str, lang: &str) -> Vec<String> {
/// article and language listed at once.
#[test]
fn every_number_of_the_quorum_texts_is_the_engine_s() {
let seed = today_seed();
let seed = m8_seed();
let f = figures(&seed);
let mut wrong = Vec::new();
for code in CODES {
@@ -821,7 +836,7 @@ fn t3_from_t2(code: &str, lang: &str, t2: &str) -> String {
#[test]
fn the_texts_of_lot_t3_change_these_phrases_only() {
let (t2, today) = (t2_seed(), today_seed());
let (t2, today) = (t2_seed(), m8_seed());
for code in CODES {
for lang in LANGS {
let (before, now) = (text(&t2, code, lang), text(&today, code, lang));
@@ -1006,7 +1021,7 @@ fn share_at(w: u32, voters: f64, params: &FormulaParams) -> f64 {
/// quorum of 2, two « pour » are asked, at every level, on every W.
#[test]
fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
let today = today_seed();
let today = m8_seed();
let said = [
(
"fr",
@@ -1104,7 +1119,7 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
/// quorum 12), while the share asked falls from 73,5 % to 73,2 %.
#[test]
fn the_share_asked_falls_beyond_the_article_s_quorum_down_to_m() {
let today = today_seed();
let today = m8_seed();
for (lang, phrase) in [
(
"fr",
@@ -1175,7 +1190,7 @@ fn the_share_asked_falls_beyond_the_article_s_quorum_down_to_m() {
/// asked of more voters than came; when everyone votes, M_k, at every quorum.
#[test]
fn the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum() {
let today = today_seed();
let today = m8_seed();
for (lang, intro) in [
(
"fr",
@@ -1247,7 +1262,7 @@ fn the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quoru
/// share asked there is s beyond the article's quorum, and lower, judged as the quorum, under it.
#[test]
fn g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum() {
let today = today_seed();
let today = m8_seed();
// The sentence of G closes N1.1, in each language; nothing of the sentence of T2 is left.
for lang in LANGS {
let (_, body) = text(&today, "N1.1", lang);
@@ -1324,7 +1339,7 @@ fn g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quor
/// reste entre » read as a fact, not as the rule that brings the median back.
#[test]
fn the_participation_habituelle_is_the_median_brought_back_within_its_bounds() {
let today = today_seed();
let today = m8_seed();
// The texts write the defined term, never the paraphrase: in N1.1, the list, its five lines and
// G (and, in French and Spanish, « comparé à la participation habituelle du groupe », already
// there); in F1.2, « Par défaut ».
@@ -1421,7 +1436,7 @@ fn the_participation_habituelle_is_the_median_brought_back_within_its_bounds() {
/// paragraph says after it bring it back. The voted text is not changed for this.
#[test]
fn the_reference_turnout_is_a_parameter_the_bounds_bring_back() {
let seed = today_seed();
let seed = m8_seed();
let f = figures(&seed);
let vote_floor = f64::from(f.reference) / 2.0;
for w in ELECTORATES.into_iter().chain([199, 200, 10_000, 10_001]) {
@@ -1629,7 +1644,7 @@ fn close(a: f64, b: f64) -> bool {
#[test]
fn the_formulas_of_the_texts_are_those_the_engine_computes() {
let seed = today_seed();
let seed = m8_seed();
for lang in LANGS {
let (_, f11) = text(&seed, "F1.1", lang);
let (_, n11) = text(&seed, "N1.1", lang);
@@ -1693,7 +1708,7 @@ fn the_formulas_of_the_texts_are_those_the_engine_computes() {
#[test]
fn the_quorum_texts_follow_the_rules_of_form() {
let seed = today_seed();
let seed = m8_seed();
let titles: HashMap<&str, Vec<&str>> = LANGS
.iter()
.map(|l| {
@@ -1812,8 +1827,8 @@ fn the_amendments_of_lot_t2_went_from_the_texts_of_m6b_to_those_of_t2() {
assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text production holds since M6c");
assert_ne!(a.from, a.to, "{code}");
// It follows lot T's, which wrote the text it replaces; lot T3's replaces it in turn,
// where it changes the text (F1.2, N1.1).
let later = AMENDMENTS.iter().find(|l| l.follows == Some(a.id)).map(|l| l.id);
// where it changes the text (F1.2, N1.1); F1.1 leaves the seed with lot T4 (ADR 0014).
let later = AMENDMENTS.iter().find(|l| l.follows == Some(a.id)).map(|l| l.id).or(Some(RETIREMENT));
assert_eq!(
(a.follows, t.to, t.superseded_by, a.superseded_by),
(Some(t.id), a.from, Some(a.id), later),
@@ -1873,7 +1888,7 @@ async fn production_after_m6(tag: &str) -> AppState {
let first = AppState::new(config(seed_dir(&format!("{tag}-born"), &born)), db.clone()).await.expect("state");
seed::documents(&first).await.expect("born");
start(&db, config(seed_dir(&format!("{tag}-m5"), &prod))).await;
let m6 = with_texts(&today_seed(), &prod, &CODES);
let m6 = with_texts(&m8_seed(), &prod, &CODES);
start(&db, config(seed_dir(&format!("{tag}-m6"), &m6))).await
}
@@ -2128,8 +2143,8 @@ async fn a_vote_a_ballot_or_a_member_s_translation_keeps_the_text_of_before() {
#[tokio::test]
async fn a_new_base_is_born_with_the_quorum_texts() {
let db = open_db("sqlite::memory:").await.expect("db");
let state = start(&db, Config::for_tests()).await;
let today = today_seed();
let state = start(&db, m8_config("new")).await;
let today = m8_seed();
for group in GROUPS {
for code in CODES {
let (_, title, body, revision) = proposal(&state, group, code).await;
@@ -2430,7 +2445,7 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_three_phrases() {
let first = AppState::new(config(seed_dir("t2-born-m5", &prod)), db.clone()).await.expect("state");
seed::documents(&first).await.expect("born");
start(&db, config(seed_dir("t2-born-m5-m5", &prod))).await;
start(&db, config(seed_dir("t2-born-m5-m6", &with_texts(&today_seed(), &prod, &CODES)))).await;
start(&db, config(seed_dir("t2-born-m5-m6", &with_texts(&m8_seed(), &prod, &CODES)))).await;
let base = start(&db, m6b_config("t2-born-m5")).await;
for group in GROUPS {
for code in CODES {
@@ -2461,7 +2476,7 @@ const T3_CODES: [&str; 2] = ["F1.2", "N1.1"];
#[test]
fn the_amendments_of_lot_t3_go_from_the_texts_of_t2_to_the_seed_of_today() {
let (t2, today) = (t2_seed(), today_seed());
let (t2, today) = (t2_seed(), m8_seed());
for code in CODES {
let t2a = amendment(code, "-phrases");
let found = AMENDMENTS.iter().find(|a| a.unit == code && a.id.ends_with("-chiffres"));
@@ -2470,7 +2485,7 @@ fn the_amendments_of_lot_t3_go_from_the_texts_of_t2_to_the_seed_of_today() {
for lang in LANGS {
assert_eq!(text(&t2, code, lang), text(&today, code, lang), "{code} {lang}");
}
assert_eq!(t2a.superseded_by, None, "{code}");
assert_eq!(t2a.superseded_by, Some(RETIREMENT), "{code}: out of the seed since lot T4");
continue;
}
let a = found.unwrap_or_else(|| panic!("{code}: the amendment of lot T3"));
@@ -2481,10 +2496,11 @@ fn the_amendments_of_lot_t3_go_from_the_texts_of_t2_to_the_seed_of_today() {
let (title, body) = text(&today, code, "fr");
assert_eq!(seed_amend::fingerprint(title, body), a.to, "{code}: the text of the seed today");
assert_ne!(a.from, a.to, "{code}");
// It follows lot T2's, which wrote the text it replaces; nothing replaces it yet.
// It follows lot T2's, which wrote the text it replaces; lot T4 takes the article out of
// the seed (ADR 0014), and with it this amendment.
assert_eq!(
(a.follows, t2a.to, t2a.superseded_by, a.superseded_by),
(Some(t2a.id), a.from, Some(a.id), None),
(Some(t2a.id), a.from, Some(a.id), Some(RETIREMENT)),
"{code}"
);
let t = amendment(code, "-quorum");
@@ -2513,7 +2529,7 @@ fn history(revision: i64) -> Vec<&'static str> {
#[tokio::test]
async fn production_after_m6c_gets_the_texts_without_dated_figures_once() {
let base = production_after_m6c("t3-apply").await;
let (t2, today) = (t2_seed(), today_seed());
let (t2, today) = (t2_seed(), m8_seed());
// The base is as production after M6c: the texts of lot T2, F1.1 at revision 3, F1.2 and
// N1.1 at revision 4.
for group in GROUPS {
@@ -2533,7 +2549,7 @@ async fn production_after_m6c_gets_the_texts_without_dated_figures_once() {
let revisions = count(&base, "SELECT COUNT(*) FROM revisions").await;
// Started with today's seed.
let state = start(&base.db, Config::for_tests()).await;
let state = start(&base.db, m8_config("t3-apply")).await;
let mut touched = Vec::new();
for group in GROUPS {
for code in T3_CODES {
@@ -2597,7 +2613,7 @@ async fn production_after_m6c_gets_the_texts_without_dated_figures_once() {
assert!(rows.iter().all(|(g, _)| g == "monnaie-libre"), "{rows:?}");
assert_eq!(count(&state, "SELECT COUNT(*) FROM outbox").await, outbox + 2);
// Decided once: a second start replays nothing.
let again = start(&state.db, Config::for_tests()).await;
let again = start(&state.db, m8_config("t3-apply-again")).await;
assert_eq!(decisions(&again, "-chiffres").await, expected);
assert_eq!(proposals(&again).await, proposals(&state).await);
assert_eq!(translations(&again).await, translations(&state).await);
@@ -2649,7 +2665,7 @@ async fn an_article_voted_on_keeps_the_text_of_t2_and_says_why() {
luna.vote(&f12_essai, Some("against")).await;
luna.vote(&f12_essai, None).await;
let state = start(&base.db, Config::for_tests()).await;
let state = start(&base.db, m8_config("t3-refuse")).await;
let refused =
|g: &str, c: &str, why: &str| (g.to_owned(), c.to_owned(), "refused".to_owned(), Some(why.to_owned()));
let applied = |g: &str, c: &str| (g.to_owned(), c.to_owned(), "applied".to_owned(), None);
@@ -2662,7 +2678,7 @@ async fn an_article_voted_on_keeps_the_text_of_t2_and_says_why() {
refused("monnaie-libre", "N1.1", "votes"),
]
);
let (t2, today) = (t2_seed(), today_seed());
let (t2, today) = (t2_seed(), m8_seed());
for group in GROUPS {
for code in T3_CODES {
let kept = (group, code) != ("essai", "F1.2");
@@ -2676,7 +2692,7 @@ async fn an_article_voted_on_keeps_the_text_of_t2_and_says_why() {
assert!(queued(&state, "-chiffres").await.is_empty());
// The refusals stand: decided once, never tried again, even once the vote is gone.
sqlx::query("DELETE FROM votes WHERE proposal_id = ?").bind(&n11).execute(&state.db).await.expect("delete");
let again = start(&state.db, Config::for_tests()).await;
let again = start(&state.db, m8_config("t3-refuse-again")).await;
assert_eq!(decisions(&again, "-chiffres").await, decisions(&state, "-chiffres").await);
let (_, title, body, _) = proposal(&again, "monnaie-libre", "N1.1").await;
assert_eq!((title.as_str(), body.as_str()), text(&t2, "N1.1", "fr"));
@@ -2691,7 +2707,7 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_texts_of_t3() {
let first = AppState::new(config(seed_dir("t3-born-m5", &prod)), db.clone()).await.expect("state");
seed::documents(&first).await.expect("born");
start(&db, config(seed_dir("t3-born-m5-m5", &prod))).await;
start(&db, config(seed_dir("t3-born-m5-m6", &with_texts(&today_seed(), &prod, &CODES)))).await;
start(&db, config(seed_dir("t3-born-m5-m6", &with_texts(&m8_seed(), &prod, &CODES)))).await;
start(&db, m6b_config("t3-born-m5")).await;
let base = start(&db, t2_config("t3-born-m5")).await;
for group in GROUPS {
@@ -2700,8 +2716,8 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_texts_of_t3() {
assert_eq!(kinds(&base, &id).await, history(3), "{group} {code}: lots T and T2");
}
}
let state = start(&db, Config::for_tests()).await;
let today = today_seed();
let state = start(&db, m8_config("t3-born-m5")).await;
let today = m8_seed();
for group in GROUPS {
for code in T3_CODES {
let (id, title, body, revision) = proposal(&state, group, code).await;
@@ -2720,8 +2736,8 @@ async fn a_base_born_with_the_texts_of_m5_gets_the_texts_of_t3() {
async fn a_base_born_with_the_texts_of_t2_gets_the_texts_of_t3() {
let db = open_db("sqlite::memory:").await.expect("db");
start(&db, t2_config("t3-born-t2")).await;
let state = start(&db, Config::for_tests()).await;
let today = today_seed();
let state = start(&db, m8_config("t3-born-t2")).await;
let today = m8_seed();
for group in GROUPS {
for code in CODES {
let (id, title, body, revision) = proposal(&state, group, code).await;