forked from yvv/decision
Lot T : F1.1, F1.2 et N1.1 disent le quorum par cran (seed, trois langues) ; un second amendement de seed gardé les remplace en production
- Le seed : « Formule du seuil WoT », « Paramètres par défaut », « Niveaux d'inertie » réécrits d'après l'ADR 0013 (sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part du cran) et l'ADR 0011 (aucun article en vigueur au départ, les trois Propositions seed au cran 2, le titre au cran 4) ; le _source le dit. - Les nombres : chacun, en fr, en et es, calculé par le moteur ou lu dans le seed (tests/seed_quorum_texts.rs) ; les trois formules évaluées telles qu'écrites contre threshold et inertia ; les règles de forme vérifiées. - seed_amend : 2026-11-03-f1-1-quorum, -f1-2-quorum, -n1-1-quorum. Un amendement qui « follows » un autre admet la révision 2 « initial puis amendment » écrite par lui seul, rien d'autre ; ceux de M5 sur F1.2 et N1.1 sont « superseded_by » (plus rien décidé, sans erreur). La garde compte aussi les lignes de bulletin du tampon (« ballots »). - Tests vus rouges : sept du lot T, deux unitaires de la garde ; les tests de M5 jouent sur le seed de M5 (la fixture de production). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,15 @@
|
||||
//! proposal instead of a text in force (ADR 0011): where an article has no text
|
||||
//! in force, `Version::InForce` examines its seed proposal (a new base: present).
|
||||
//!
|
||||
//! Since 2026-11-03 (lot T), a text may be amended twice: F1.1, F1.2 and N1.1 say
|
||||
//! the quorum by level (ADR 0013) instead of the floor C, and F1.2 and N1.1 had
|
||||
//! been rewritten by the amendments of 02/11. An amendment that `follows` an
|
||||
//! earlier one replaces the text that one wrote: it admits, besides the seed's
|
||||
//! own revision 1, the revision 2 whose history is « initial » then « amendment »
|
||||
//! and on which that earlier amendment, and it alone, was applied — nothing else.
|
||||
//! The earlier one is then `superseded_by` it: the seed no longer gives its text,
|
||||
//! so it decides nothing more, without an error (the decisions already taken stay).
|
||||
//!
|
||||
//! A seeded document is never instantiated again: once in the database, it is
|
||||
//! runtime data. When the seed's text of an article changes (Yvv, 2026-10-30:
|
||||
//! the Préambule becomes « Raison d'être »), new bases get it from the seed;
|
||||
@@ -14,9 +23,11 @@
|
||||
//! nobody has touched the article**:
|
||||
//!
|
||||
//! - its version in force is the seed's own, a *seed a minima* (never voted
|
||||
//! into force, never confirmed), at revision 1;
|
||||
//! - nobody ever voted on it, corrected it, edited one of its translations or
|
||||
//! named it in a contradiction signal;
|
||||
//! into force, never confirmed), at revision 1 — or 2, after the amendment it
|
||||
//! follows;
|
||||
//! - nobody ever voted on it (a vote, or a line of a ballot of the buffer, pending
|
||||
//! or proven, whatever became of it: ADR 0012), corrected it, edited one of its
|
||||
//! translations or named it in a contradiction signal (an objection);
|
||||
//! - its text is exactly the one the amendment replaces (fingerprint).
|
||||
//!
|
||||
//! Each document is decided once, at the first start that knows the
|
||||
@@ -58,6 +69,12 @@ pub struct Amendment {
|
||||
pub to: &'static str,
|
||||
/// Reason given to the publication queue, and so to the commit of the texts.
|
||||
pub reason: &'static str,
|
||||
/// The earlier amendment of the same text whose text this one replaces, if any:
|
||||
/// its revision 2 is then admitted (see the module).
|
||||
pub follows: Option<&'static str>,
|
||||
/// The later amendment whose text the seed gives instead, if any: this one then
|
||||
/// decides nothing more, without an error.
|
||||
pub superseded_by: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub const AMENDMENTS: &[Amendment] = &[
|
||||
@@ -69,6 +86,8 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
from: "ac17693c61b678ae48f5386f3d528a78385a62f3e2bb12a802a21f45c762b375",
|
||||
to: "a7095d70602a0fc60d0904e9ffbce6d8520914ce3cb424d8f75e48bf6cfdf41a",
|
||||
reason: "texte de départ remplacé : I1",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
},
|
||||
// The rule articles, open proposals of the seed, after the rules applied (Yvv, 2026-11-02).
|
||||
Amendment {
|
||||
@@ -79,6 +98,8 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
from: "044c65d6b97e4278184cb1acfe6fa2227ddfe91465c717d8a81f52092314fae6",
|
||||
to: "b24ff4d1c95cfbaae0231885009ee3d600957a3ce498bede80b29bceca651a60",
|
||||
reason: "proposition de départ réécrite : F1.2",
|
||||
follows: None,
|
||||
superseded_by: Some("2026-11-03-f1-2-quorum"),
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-02-n1-regles",
|
||||
@@ -88,6 +109,8 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
from: "eed13441afae8d33eba37545163037ccf89b0801d84072ef55290575a1410142",
|
||||
to: "c1facdfd0c0f3aaa94b74192e146557558fe88f66304dc6d21279714d7fb727e",
|
||||
reason: "proposition de départ réécrite : N1",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-02-n1-1-regles",
|
||||
@@ -97,6 +120,8 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
from: "d5d06c5ae5f3a1c1b08e3c3721b2b09c3dcfd528c11d16d3368d6e14d8942bee",
|
||||
to: "865ed24d5eb18745a8c35dc727247219c449e078d39b6a18ec2fdcfd4f8289a0",
|
||||
reason: "proposition de départ réécrite : N1.1",
|
||||
follows: None,
|
||||
superseded_by: Some("2026-11-03-n1-1-quorum"),
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-02-o1-regles",
|
||||
@@ -106,6 +131,44 @@ pub const AMENDMENTS: &[Amendment] = &[
|
||||
from: "40ab4c06a9e374bded6023b9dffbee5736fc4000d422c5a819f35cdb00b02642",
|
||||
to: "4173738055af10fceb74bb6e954bdfefd7ab364480d49afbc92ea5f68825c537",
|
||||
reason: "proposition de départ réécrite : O1",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
},
|
||||
// The articles that still described the floor C, after the quorum by level (ADR 0013;
|
||||
// Yvv, 2026-11-02 and 03; lot T). F1.1 was never amended; F1.2 and N1.1 replace the
|
||||
// text the amendments of 02/11 wrote.
|
||||
Amendment {
|
||||
id: "2026-11-03-f1-1-quorum",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "F1.1",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "258980be190192fe3c7b32f6319584b682621d01329d880b7056c28c84e9db0a",
|
||||
to: "ca6d3c74a3e6de22ebf55eb0cdd6465dbe22d69cd86aae89ac335b960671b4fa",
|
||||
reason: "proposition de départ réécrite : F1.1, le quorum par cran",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-03-f1-2-quorum",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "F1.2",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "b24ff4d1c95cfbaae0231885009ee3d600957a3ce498bede80b29bceca651a60",
|
||||
to: "66a9bc125c78120a76a81e8b53c427f4d2d5895581cb4ed4ade1aaab740944a9",
|
||||
reason: "proposition de départ réécrite : F1.2, le quorum par cran",
|
||||
follows: Some("2026-11-02-f1-2-regles"),
|
||||
superseded_by: None,
|
||||
},
|
||||
Amendment {
|
||||
id: "2026-11-03-n1-1-quorum",
|
||||
doc: "acte-engagement-certification",
|
||||
unit: "N1.1",
|
||||
version: Version::Open("1.0.0"),
|
||||
from: "865ed24d5eb18745a8c35dc727247219c449e078d39b6a18ec2fdcfd4f8289a0",
|
||||
to: "1bdd26a63cadb4529f5347863ced83a823439f53247b6aea8e703301bab3de48",
|
||||
reason: "proposition de départ réécrite : N1.1, le quorum par cran",
|
||||
follows: Some("2026-11-02-n1-1-regles"),
|
||||
superseded_by: None,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -125,6 +188,10 @@ pub struct Facts {
|
||||
pub revision: i64,
|
||||
/// Votes cast on it, whatever their revision.
|
||||
pub votes: i64,
|
||||
/// Lines of ballots of the buffer naming it, pending or proven, whatever became of them.
|
||||
pub ballots: i64,
|
||||
/// The seed amendments applied on it, in order.
|
||||
pub amendments: Vec<String>,
|
||||
/// Kinds of its revisions, in order.
|
||||
pub revisions: Vec<String>,
|
||||
/// Translations written or corrected by a member.
|
||||
@@ -155,17 +222,22 @@ pub fn verdict(facts: Option<&Facts>, a: &Amendment) -> Verdict {
|
||||
if f.fingerprint == a.to {
|
||||
return Verdict::Present;
|
||||
}
|
||||
// The text the amendment follows: written by it, and by it alone, at revision 2.
|
||||
let after_earlier = a.follows.is_some_and(|id| f.revision == 2 && f.amendments == [id]);
|
||||
let history: &[&str] = if after_earlier { &["initial", "amendment"] } else { &["initial"] };
|
||||
let refusal = if f.origin != "seed" {
|
||||
"not_a_seed"
|
||||
} else if f.entered {
|
||||
"voted_into_force"
|
||||
} else if f.confirmed {
|
||||
"confirmed"
|
||||
} else if f.revision != 1 {
|
||||
} else if f.revision != 1 && !after_earlier {
|
||||
"revised"
|
||||
} else if f.votes > 0 {
|
||||
"votes"
|
||||
} else if f.revisions != ["initial"] || f.typos > 0 {
|
||||
} else if f.ballots > 0 {
|
||||
"ballots"
|
||||
} else if f.revisions != history || f.typos > 0 {
|
||||
"corrected"
|
||||
} else if f.member_translations > 0 {
|
||||
"translation_edited"
|
||||
@@ -234,6 +306,15 @@ fn target_of<'a>(seeds: &'a [SeedDoc], a: &Amendment) -> Option<(&'a SeedDoc, &'
|
||||
let text = version.and_then(|v| Some((v.title.get(src)?, v.body.get(src)?)));
|
||||
match (version, text) {
|
||||
(Some(v), Some((title, body))) if fingerprint(title, body) == a.to => Some((seed, v)),
|
||||
_ if a.superseded_by.is_some() => {
|
||||
tracing::debug!(
|
||||
amendment = a.id,
|
||||
unit = a.unit,
|
||||
superseded_by = a.superseded_by,
|
||||
"seed amendment superseded: nothing decided"
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
tracing::error!(
|
||||
amendment = a.id,
|
||||
@@ -294,12 +375,25 @@ pub(crate) async fn facts(
|
||||
.bind(proposal_id)
|
||||
.fetch_one(&mut *conn)
|
||||
.await?;
|
||||
// Every line of a ballot, chain or simulated, whatever its fate: a vote was cast.
|
||||
let ballots: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM ballot_lines WHERE proposal_id = ?")
|
||||
.bind(proposal_id)
|
||||
.fetch_one(&mut *conn)
|
||||
.await?;
|
||||
let amendments: Vec<String> = sqlx::query_scalar(
|
||||
"SELECT amendment_id FROM seed_amendments WHERE proposal_id = ? AND outcome = 'applied' ORDER BY at, rowid",
|
||||
)
|
||||
.bind(proposal_id)
|
||||
.fetch_all(&mut *conn)
|
||||
.await?;
|
||||
Ok(Some(Facts {
|
||||
origin: p.origin,
|
||||
entered: p.entered_at.is_some(),
|
||||
confirmed: p.confirmed_at.is_some(),
|
||||
revision: p.revision,
|
||||
votes,
|
||||
ballots,
|
||||
amendments,
|
||||
revisions,
|
||||
member_translations,
|
||||
signals,
|
||||
@@ -358,6 +452,7 @@ async fn decide(
|
||||
};
|
||||
let mut v = verdict(found.as_ref(), a);
|
||||
let now = util::now();
|
||||
let judged = found.as_ref().map_or(1, |f| f.revision);
|
||||
if let (Verdict::Apply, Some((pid, status))) = (v, &examined) {
|
||||
let src = seed.source_lang.as_str();
|
||||
let pick = |m: &util::LangMap| m.get(src).cloned().unwrap_or_default();
|
||||
@@ -366,21 +461,23 @@ async fn decide(
|
||||
let done = sqlx::query(
|
||||
"UPDATE proposals SET title = ?, body = ?, revision = revision + 1, updated_at = ?
|
||||
WHERE id = ? AND status = ? AND origin = 'seed' AND entered_at IS NULL
|
||||
AND confirmed_at IS NULL AND revision = 1",
|
||||
AND confirmed_at IS NULL AND revision = ?",
|
||||
)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(&now)
|
||||
.bind(pid)
|
||||
.bind(*status)
|
||||
.bind(judged)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if done.rows_affected() == 1 {
|
||||
sqlx::query(
|
||||
"INSERT INTO revisions (proposal_id, revision, kind, title, body, author_id, created_at)
|
||||
VALUES (?, 2, 'amendment', ?, ?, NULL, ?)",
|
||||
VALUES (?, ?, 'amendment', ?, ?, NULL, ?)",
|
||||
)
|
||||
.bind(pid)
|
||||
.bind(judged + 1)
|
||||
.bind(&title)
|
||||
.bind(&body)
|
||||
.bind(&now)
|
||||
@@ -451,6 +548,8 @@ mod tests {
|
||||
from: "old",
|
||||
to: "new",
|
||||
reason: "texte de départ remplacé : I1",
|
||||
follows: None,
|
||||
superseded_by: None,
|
||||
};
|
||||
|
||||
fn intact() -> Facts {
|
||||
@@ -497,5 +596,54 @@ mod tests {
|
||||
let open = Amendment { version: Version::Open("1.0.0"), ..A };
|
||||
assert_eq!(verdict(None, &open), Verdict::Refuse("no_open_version"));
|
||||
assert_eq!(verdict(Some(&intact()), &open), Verdict::Apply);
|
||||
// A ballot of the buffer names it, pending or proven, whatever became of it (ADR 0012).
|
||||
assert_eq!(verdict(Some(&Facts { ballots: 1, ..intact() }), &A), Verdict::Refuse("ballots"));
|
||||
}
|
||||
|
||||
/// The text an earlier amendment wrote, as production holds F1.2 and N1.1 since M5.
|
||||
fn amended_by(earlier: &str) -> Facts {
|
||||
Facts {
|
||||
revision: 2,
|
||||
revisions: vec!["initial".into(), "amendment".into()],
|
||||
amendments: vec![earlier.into()],
|
||||
..intact()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_amendment_admits_the_text_the_first_wrote_and_nothing_else() {
|
||||
// Lot T (2026-11-03): F1.2 and N1.1 were amended by M5 (revision 2); the quorum
|
||||
// texts replace them, after that amendment only.
|
||||
let second = Amendment { version: Version::Open("1.0.0"), follows: Some("m5"), ..A };
|
||||
assert_eq!(verdict(Some(&amended_by("m5")), &second), Verdict::Apply);
|
||||
// A base born with the text of M5: its revision 1, as the seed made it.
|
||||
assert_eq!(verdict(Some(&intact()), &second), Verdict::Apply);
|
||||
// Not the amendment it follows, or none: a revision 2 of something else.
|
||||
assert_eq!(verdict(Some(&amended_by("other")), &second), Verdict::Refuse("revised"));
|
||||
let typo = Facts { revisions: vec!["initial".into(), "typo".into()], amendments: vec![], ..amended_by("m5") };
|
||||
assert_eq!(verdict(Some(&typo), &second), Verdict::Refuse("revised"));
|
||||
// A history that is not « initial then amendment ».
|
||||
let odd = Facts { revisions: vec!["initial".into(), "substantive".into()], ..amended_by("m5") };
|
||||
assert_eq!(verdict(Some(&odd), &second), Verdict::Refuse("corrected"));
|
||||
// Further on, nothing is admitted.
|
||||
let third = Facts {
|
||||
revision: 3,
|
||||
revisions: vec!["initial".into(), "amendment".into(), "typo".into()],
|
||||
..amended_by("m5")
|
||||
};
|
||||
assert_eq!(verdict(Some(&third), &second), Verdict::Refuse("revised"));
|
||||
// The same traces of a person keep it.
|
||||
for (f, why) in [
|
||||
(Facts { votes: 1, ..amended_by("m5") }, "votes"),
|
||||
(Facts { ballots: 2, ..amended_by("m5") }, "ballots"),
|
||||
(Facts { typos: 1, ..amended_by("m5") }, "corrected"),
|
||||
(Facts { member_translations: 1, ..amended_by("m5") }, "translation_edited"),
|
||||
(Facts { signals: 1, ..amended_by("m5") }, "signalled"),
|
||||
(Facts { fingerprint: "other".into(), ..amended_by("m5") }, "text_differs"),
|
||||
] {
|
||||
assert_eq!(verdict(Some(&f), &second), Verdict::Refuse(why), "{f:?}");
|
||||
}
|
||||
// An amendment that follows none never admits a revision 2.
|
||||
assert_eq!(verdict(Some(&amended_by("m5")), &A), Verdict::Refuse("revised"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ use crate::util;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
const REFRESH: Duration = Duration::from_secs(6 * 3600);
|
||||
/// W is read again this often.
|
||||
pub const REFRESH: Duration = Duration::from_secs(6 * 3600);
|
||||
const PLAUSIBLE: std::ops::RangeInclusive<u32> = 100..=10_000_000;
|
||||
|
||||
fn twox_128(data: &[u8]) -> [u8; 16] {
|
||||
|
||||
@@ -353,9 +353,13 @@ async fn the_production_base_turns_its_untouched_texts_into_seed_proposals_once(
|
||||
kept.push(v);
|
||||
}
|
||||
}
|
||||
// The articles of the quorum (F1.1, F1.2, N1.1) are amended at the same start, by the
|
||||
// seed amendments of lot T (`tests/seed_quorum_texts.rs`): left out here.
|
||||
let translations = |state: AppState| async move {
|
||||
sqlx::query_as::<_, (String, String, String, String, String)>(
|
||||
"SELECT proposal_id, lang, title, body, source_hash FROM translations ORDER BY proposal_id, lang",
|
||||
"SELECT t.proposal_id, t.lang, t.title, t.body, t.source_hash FROM translations t
|
||||
JOIN proposals p ON p.id = t.proposal_id JOIN units u ON u.id = p.unit_id
|
||||
WHERE u.code NOT IN ('F1.1', 'F1.2', 'N1.1') ORDER BY t.proposal_id, t.lang",
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
||||
//! The four rule articles of the seed rewritten after the rules applied (Yvv,
|
||||
//! 2026-11-02): F1.2, N1, N1.1 and O1 are open proposals at zero votes, not texts
|
||||
//! in force; a guarded seed amendment replaces them where nobody touched them.
|
||||
//!
|
||||
//! Played with the seed M5 shipped (3f10a0e, the fixture of production): since lot T
|
||||
//! (2026-11-03), today's seed gives F1.2 and N1.1 the texts of the quorum, and their
|
||||
//! amendments of 02/11 are superseded (`tests/seed_quorum_texts.rs`).
|
||||
|
||||
use ld_server::seed_amend::{self, AMENDMENTS, Version};
|
||||
use ld_server::{AppState, Config, open_db, seed};
|
||||
@@ -21,6 +25,15 @@ fn today_seed() -> Value {
|
||||
read(Config::for_tests().seed_dir.join("acte-engagement-certification.json"))
|
||||
}
|
||||
|
||||
/// The seed M5 shipped: production's since 2026-11-02.
|
||||
fn m5_dir() -> std::path::PathBuf {
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/seed-production-2026-11-02")
|
||||
}
|
||||
|
||||
fn m5_seed() -> Value {
|
||||
read(m5_dir().join("acte-engagement-certification.json"))
|
||||
}
|
||||
|
||||
fn version<'a>(seed: &'a Value, code: &str) -> &'a Value {
|
||||
let u = seed["units"].as_array().expect("units").iter().find(|u| u["code"] == code).expect("unit");
|
||||
&u["versions"][0]
|
||||
@@ -30,12 +43,12 @@ fn fr(v: &Value) -> (&str, &str) {
|
||||
(v["title"]["fr"].as_str().expect("title"), v["body"]["fr"].as_str().expect("body"))
|
||||
}
|
||||
|
||||
/// Today's seed with the four rule articles of before.
|
||||
/// The seed of M5 with the four rule articles of before.
|
||||
fn seed_dir_before(tag: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("ld-seed-rules-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("dir");
|
||||
let mut seed = today_seed();
|
||||
let mut seed = m5_seed();
|
||||
let old = before();
|
||||
for u in seed["units"].as_array_mut().expect("units") {
|
||||
let code = u["code"].as_str().expect("code").to_owned();
|
||||
@@ -59,9 +72,11 @@ async fn base_before(tag: &str) -> AppState {
|
||||
state
|
||||
}
|
||||
|
||||
/// The same base, started again with today's seed and its amendments.
|
||||
/// The same base, started again with the seed of M5 and its amendments.
|
||||
async fn restart(before: &AppState) -> AppState {
|
||||
let state = AppState::new(Config::for_tests(), before.db.clone()).await.expect("state");
|
||||
let mut cfg = Config::for_tests();
|
||||
cfg.seed_dir = m5_dir();
|
||||
let state = AppState::new(cfg, before.db.clone()).await.expect("state");
|
||||
seed::ensure(&state).await.expect("seed");
|
||||
state
|
||||
}
|
||||
@@ -92,19 +107,29 @@ async fn decisions(state: &AppState) -> Vec<(String, String, String, Option<Stri
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shipped_amendments_go_from_the_texts_of_before_to_the_seed_of_today() {
|
||||
let (old, today) = (before(), today_seed());
|
||||
fn the_shipped_amendments_go_from_the_texts_of_before_to_the_seed_of_m5() {
|
||||
let (old, m5, today) = (before(), m5_seed(), today_seed());
|
||||
for code in CODES {
|
||||
let a = AMENDMENTS.iter().find(|a| a.unit == code).unwrap_or_else(|| panic!("no amendment for {code}"));
|
||||
assert_eq!(a.doc, "acte-engagement-certification");
|
||||
assert_eq!(a.version, Version::Open("1.0.0"), "{code}: the open proposal of the seed");
|
||||
let (t, b) = fr(&old["units"][code]);
|
||||
assert_eq!(seed_amend::fingerprint(t, b), a.from, "{code}: the text production holds");
|
||||
let v = version(&today, code);
|
||||
let v = version(&m5, code);
|
||||
assert_eq!((v["status"].as_str(), v["label"].as_str()), (Some("open"), Some("1.0.0")), "{code}");
|
||||
let (t, b) = fr(v);
|
||||
assert_eq!(seed_amend::fingerprint(t, b), a.to, "{code}: the text of the seed today");
|
||||
assert_eq!(seed_amend::fingerprint(t, b), a.to, "{code}: the text of the seed of M5");
|
||||
assert_ne!(a.from, a.to, "{code}");
|
||||
// Today's seed gives it, or another text whose amendment follows this one.
|
||||
let (t, b) = fr(version(&today, code));
|
||||
let now = seed_amend::fingerprint(t, b);
|
||||
match a.superseded_by {
|
||||
None => assert_eq!(now, a.to, "{code}: still the text of today"),
|
||||
Some(later) => {
|
||||
let l = AMENDMENTS.iter().find(|l| l.id == later).expect("the later amendment");
|
||||
assert_eq!((l.unit, l.follows, l.from, l.to), (code, Some(a.id), a.to, now.as_str()), "{code}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +148,7 @@ async fn untouched_rule_articles_are_replaced_once_and_a_vote_keeps_its_text() {
|
||||
.await
|
||||
.expect("vote");
|
||||
let state = restart(&base).await;
|
||||
let (old, today) = (before(), today_seed());
|
||||
let (old, today) = (before(), m5_seed());
|
||||
for group in ["essai", "monnaie-libre"] {
|
||||
for code in CODES {
|
||||
let (id, title, body, revision) = proposal(&state, group, code).await;
|
||||
@@ -202,6 +227,9 @@ async fn a_new_base_is_born_with_the_rules_of_today() {
|
||||
assert_eq!(((title.as_str(), body.as_str()), revision), (fr(version(&today, code)), 1), "{group} {code}");
|
||||
}
|
||||
}
|
||||
assert!(decisions(&state).await.iter().all(|(_, _, o, _)| o == "present"));
|
||||
assert_eq!(decisions(&state).await.len(), 8);
|
||||
// N1 and O1: present. F1.2 and N1.1: their amendments, superseded, decide nothing.
|
||||
let got = decisions(&state).await;
|
||||
assert!(got.iter().all(|(_, _, o, _)| o == "present"), "{got:?}");
|
||||
let units: Vec<&str> = got.iter().map(|(_, u, _, _)| u.as_str()).collect();
|
||||
assert_eq!(units, ["N1", "O1", "N1", "O1"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user