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
+73 -33
View File
@@ -42,8 +42,7 @@ const LOW: u8 = ld_server::seed::SEED_PROPOSAL_LEVEL;
/// he wrote « il n'y a une et une seule » (read as a slip, the parallel of the first half);
/// the quotation keeps the non-breaking spaces of the one it replaces.
const I1_FR: &str = "*«\u{a0}Derrière une clé il y a un et un seul être humain vivant\u{a0}; derrière un être humain, il y a une et une seule clé certifiante.\u{a0}»*\n\nC'est l'unique enjeu de la certification et de votre engagement, il donne à notre toile fiduciaire sa valeur : pouvoir nous fier à l'unicité et l'intégrité de tout geste signé par cette clé.\n\nCet acte d'engagement est par ailleurs le seul acte de nature contractuelle, qui fonde l'usage de la première monnaie libre.";
const E1_FR: &str =
"Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte certifiant.";
const E1_FR: &str = "Je m'engage sur l'honneur à n'avoir et n'avoir jamais qu'un seul et unique compte certifiant.";
const E12_TITLE: [(&str, &str); 3] =
[("fr", "Responsabilité civique"), ("en", "Civic responsibility"), ("es", "Responsabilidad cívica")];
const E12_FR: &str = "Je déclare sur l'honneur avoir la capacité légale et physique de signer un contrat et d'en assumer la responsabilité civique, pour certifier à mon tour.";
@@ -364,22 +363,28 @@ async fn rows(state: &AppState, table: &str, skip: &[&str], filter: &str) -> Vec
.fetch_all(&state.db)
.await
.expect("columns");
let cols: Vec<String> = columns.iter().filter(|c| !skip.contains(&c.as_str())).map(|c| format!("quote(\"{c}\")")).collect();
let cols: Vec<String> =
columns.iter().filter(|c| !skip.contains(&c.as_str())).map(|c| format!("quote(\"{c}\")")).collect();
let sql = format!("SELECT {} FROM \"{table}\" WHERE {filter} ORDER BY rowid", cols.join(" || '|' || "));
sqlx::query_scalar(&sql).fetch_all(&state.db).await.expect("rows")
}
/// The units that leave, by section (their articles and their orders).
const OF_THE_OUT: &str = "section IN ('formule', 'inertie', 'ordonnancement')";
const OF_THEIR_VERSIONS: &str =
"proposal_id IN (SELECT p.id FROM proposals p JOIN units u ON u.id = p.unit_id WHERE u.section IN ('formule', 'inertie', 'ordonnancement'))";
const OF_THEIR_VERSIONS: &str = "proposal_id IN (SELECT p.id FROM proposals p JOIN units u ON u.id = p.unit_id WHERE u.section IN ('formule', 'inertie', 'ordonnancement'))";
/// What the articles of operation hold: units (without the columns of the retirement),
/// their versions, revisions, translations.
async fn the_rules_of_operation(state: &AppState) -> [Vec<String>; 4] {
[
rows(state, "units", &["retired_at", "retired_reason"], OF_THE_OUT).await,
rows(state, "proposals", &[], "unit_id IN (SELECT id FROM units WHERE section IN ('formule', 'inertie', 'ordonnancement'))").await,
rows(
state,
"proposals",
&[],
"unit_id IN (SELECT id FROM units WHERE section IN ('formule', 'inertie', 'ordonnancement'))",
)
.await,
rows(state, "revisions", &[], OF_THEIR_VERSIONS).await,
rows(state, "translations", &[], OF_THEIR_VERSIONS).await,
]
@@ -461,7 +466,11 @@ async fn a_new_base_is_born_with_the_texts_of_t4_and_without_the_rules_of_operat
let u = unit(&doc, code).expect(code);
let p = &u["proposals"][0];
assert_eq!(p["body"], new_text(code), "{group} {code}");
assert_eq!((p["status"].as_str(), p["seedProposal"].as_bool()), (Some("open"), Some(true)), "{group} {code}");
assert_eq!(
(p["status"].as_str(), p["seedProposal"].as_bool()),
(Some("open"), Some(true)),
"{group} {code}"
);
assert_eq!(p["revision"], 1, "{group} {code}");
assert_eq!(u["inertia"], LOW, "{group} {code}");
assert!(u["inForce"].is_null(), "{group} {code}");
@@ -489,7 +498,10 @@ async fn a_new_base_is_born_with_the_texts_of_t4_and_without_the_rules_of_operat
let present = |g: &str| (g.to_owned(), "present".to_owned(), None, None);
assert_eq!(retirements(&state).await, [present("essai"), present("monnaie-libre")]);
let added = additions(&state).await;
assert_eq!(added.iter().map(|a| (a.0.as_str(), a.1.as_str())).collect::<Vec<_>>(), [("essai", "present"), ("monnaie-libre", "present")]);
assert_eq!(
added.iter().map(|a| (a.0.as_str(), a.1.as_str())).collect::<Vec<_>>(),
[("essai", "present"), ("monnaie-libre", "present")]
);
let rw = rewrites(&state).await;
assert_eq!(rw.len(), 6, "{rw:?}");
assert!(rw.iter().all(|(_, _, o, _)| o == "present"), "{rw:?}");
@@ -507,11 +519,8 @@ async fn production_gets_lot_t4_once(base: AppState, rules_revision: i64) {
for code in AMORCE {
let (id, status, revision, _, _, featured) = seed_version(&base, group, code).await;
assert_eq!((status.as_str(), featured), ("open", true), "{group} {code}");
let expected: &[&str] = if code == "I1" {
&["initial", "amendment", "seed_proposal"]
} else {
&["initial", "seed_proposal"]
};
let expected: &[&str] =
if code == "I1" { &["initial", "amendment", "seed_proposal"] } else { &["initial", "seed_proposal"] };
assert_eq!(kinds(&base, &id).await, expected, "{group} {code}");
assert_eq!(revision, if code == "I1" { 2 } else { 1 }, "{group} {code}");
}
@@ -523,7 +532,9 @@ async fn production_gets_lot_t4_once(base: AppState, rules_revision: i64) {
let before = tables(&base).await;
let before_rows = rowids(&base).await;
let kept = the_rules_of_operation(&base).await;
let untouched_versions = rows(&base, "proposals", &[], "unit_id NOT IN (SELECT u.id FROM units u WHERE u.code IN ('I1', 'E1', 'E2'))").await;
let untouched_versions =
rows(&base, "proposals", &[], "unit_id NOT IN (SELECT u.id FROM units u WHERE u.code IN ('I1', 'E1', 'E2'))")
.await;
let state = t4(&base).await;
// Taken at once, before anyone signs in (a session writes its identity).
@@ -547,7 +558,11 @@ async fn production_gets_lot_t4_once(base: AppState, rules_revision: i64) {
let v = &seed_unit(&today, code)["versions"][0];
for lang in ["en", "es"] {
let (t, b, hash, origin, by) = translation(&state, &id, lang).await;
assert_eq!((json!(t), json!(b)), (v["title"][lang].clone(), v["body"][lang].clone()), "{group} {code} {lang}");
assert_eq!(
(json!(t), json!(b)),
(v["title"][lang].clone(), v["body"][lang].clone()),
"{group} {code} {lang}"
);
assert_eq!(hash, ld_server::seed_amend::fingerprint(&title, &body), "{group} {code} {lang}");
assert_eq!((origin.as_str(), by), ("seed", None));
}
@@ -565,14 +580,17 @@ async fn production_gets_lot_t4_once(base: AppState, rules_revision: i64) {
assert_eq!((title.as_str(), body.as_str()), ("Responsabilité civique", E12_FR), "{group}");
assert_eq!((status.as_str(), revision, featured), ("open", 1, true), "{group}");
assert_eq!(kinds(&state, &id).await, ["initial"], "{group}");
let (section, position, inertia, in_force): (String, f64, String, Option<String>) = sqlx::query_as(
"SELECT section, position, inertia, in_force_id FROM units WHERE id = ?",
)
.bind(unit_id(&state, group, NEW).await)
.fetch_one(&state.db)
.await
.expect("unit");
assert_eq!((section.as_str(), position, inertia, in_force), ("fondamental", 4.5, LOW.to_string(), None), "{group}");
let (section, position, inertia, in_force): (String, f64, String, Option<String>) =
sqlx::query_as("SELECT section, position, inertia, in_force_id FROM units WHERE id = ?")
.bind(unit_id(&state, group, NEW).await)
.fetch_one(&state.db)
.await
.expect("unit");
assert_eq!(
(section.as_str(), position, inertia, in_force),
("fondamental", 4.5, LOW.to_string(), None),
"{group}"
);
for (lang, t) in E12_TITLE.iter().skip(1) {
let (title_tr, _, hash, origin, _) = translation(&state, &id, lang).await;
assert_eq!((title_tr.as_str(), origin.as_str()), (*t, "seed"), "{group} {lang}");
@@ -586,10 +604,11 @@ async fn production_gets_lot_t4_once(base: AppState, rules_revision: i64) {
for group in GROUPS {
assert_eq!(retired(&state, group).await, all_out(), "{group}");
}
let reasons: Vec<String> = sqlx::query_scalar(&format!("SELECT DISTINCT retired_reason FROM units WHERE {OF_THE_OUT}"))
.fetch_all(&state.db)
.await
.expect("reasons");
let reasons: Vec<String> =
sqlx::query_scalar(&format!("SELECT DISTINCT retired_reason FROM units WHERE {OF_THE_OUT}"))
.fetch_all(&state.db)
.await
.expect("reasons");
assert_eq!(reasons, [RETIRE]);
assert_eq!(the_rules_of_operation(&state).await, kept, "their rows, as they were");
@@ -636,9 +655,22 @@ async fn production_gets_lot_t4_once(base: AppState, rules_revision: i64) {
let was = before.get(t).copied().unwrap_or(0);
assert_eq!(n - was, added.get(t.as_str()).copied().unwrap_or(0), "{t}: {was} → {n}");
}
eprintln!("lot T4, rows by table (before → after): {}", after.iter().map(|(t, n)| format!("{t} {} → {n}", before.get(t).copied().unwrap_or(0))).collect::<Vec<_>>().join(", "));
eprintln!(
"lot T4, rows by table (before → after): {}",
after
.iter()
.map(|(t, n)| format!("{t} {} → {n}", before.get(t).copied().unwrap_or(0)))
.collect::<Vec<_>>()
.join(", ")
);
// Every other version untouched.
let others = rows(&state, "proposals", &[], "unit_id NOT IN (SELECT u.id FROM units u WHERE u.code IN ('I1', 'E1', 'E2', 'E12'))").await;
let others = rows(
&state,
"proposals",
&[],
"unit_id NOT IN (SELECT u.id FROM units u WHERE u.code IN ('I1', 'E1', 'E2', 'E12'))",
)
.await;
assert_eq!(others, untouched_versions);
// The queue of Monnaie libre: a row for each change, waiting for the first text.
let queued: Vec<(String, String)> = sqlx::query_as(
@@ -657,7 +689,10 @@ async fn production_gets_lot_t4_once(base: AppState, rules_revision: i64) {
let again = t4(&state).await;
assert_eq!(tables(&again).await, settled);
let unchanged = |t: &BTreeMap<String, i64>| {
t.iter().filter(|(k, _)| !matches!(k.as_str(), "identities" | "sessions" | "memberships")).map(|(k, n)| (k.clone(), *n)).collect::<Vec<_>>()
t.iter()
.filter(|(k, _)| !matches!(k.as_str(), "identities" | "sessions" | "memberships"))
.map(|(k, n)| (k.clone(), *n))
.collect::<Vec<_>>()
};
assert_eq!(unchanged(&settled), unchanged(&after));
assert_eq!(retirements(&again).await, [mark("essai"), mark("monnaie-libre")]);
@@ -843,7 +878,8 @@ async fn leave(state: &AppState, trace: Trace) {
Trace::Typo => {
let (p, _, _, title, body, _) = id("F1.1").await;
let fixed = body.replacen('\'', "\u{2019}", 1);
let v = vera.ok("PATCH", &format!("/api/proposals/{p}"), Some(json!({ "title": title, "body": fixed }))).await;
let v =
vera.ok("PATCH", &format!("/api/proposals/{p}"), Some(json!({ "title": title, "body": fixed }))).await;
assert_eq!(v["edit"], "typo_pending", "{v}");
}
Trace::Translation => {
@@ -1002,7 +1038,8 @@ async fn an_image_that_ignores_the_column_shows_the_rules_of_operation_as_they_w
let base = production_after_m8("rollback").await;
let mut c = Client::as_(&base, "Chiara", false).await;
let view = |doc: &Value| -> Vec<Value> {
let units = doc["units"].as_array().expect("units").iter().filter(|u| OUT.contains(&u["code"].as_str().unwrap_or("")));
let units =
doc["units"].as_array().expect("units").iter().filter(|u| OUT.contains(&u["code"].as_str().unwrap_or("")));
let orders = doc["modules"]["orders"]
.as_array()
.expect("orders")
@@ -1021,7 +1058,10 @@ async fn an_image_that_ignores_the_column_shows_the_rules_of_operation_as_they_w
assert!(view(&c.doc(group).await).is_empty(), "{group}: retired");
}
// The image of before (de1b7eb) reads `units` without the column: as here with it empty.
sqlx::query("UPDATE units SET retired_at = NULL").execute(&state.db).await.expect("as the image of before reads it");
sqlx::query("UPDATE units SET retired_at = NULL")
.execute(&state.db)
.await
.expect("as the image of before reads it");
let mut c = Client::as_(&state, "Chiara", false).await;
for (i, group) in GROUPS.iter().enumerate() {
assert_eq!(view(&c.doc(group).await), before[i], "{group}: the same units, versions, texts, translations");