Sprint 1 : scaffolding complet de Glibredecision

Plateforme de decisions collectives pour Duniter/G1.
Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services,
moteur de vote avec formule d'inertie WoT/Smith/TechComm).
Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores).
Infrastructure Docker + Woodpecker CI + Traefik.
Documentation technique et utilisateur (15 fichiers).
Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote.
30 tests unitaires (formules, mode params, vote nuance) -- tous verts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-02-28 12:46:11 +01:00
commit 25437f24e3
100 changed files with 10236 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
"""Tests for WoT threshold formula, Smith threshold, and TechComm threshold.
Real-world reference case:
Vote Engagement Forgeron v2.0.0 (Feb 2026)
wot_size=7224, votes_for=97, votes_against=23, total=120
params M=50, B=0.1, G=0.2 => threshold=94 => adopted (97 >= 94)
"""
import math
import pytest
from app.engine.threshold import wot_threshold
from app.engine.smith_threshold import smith_threshold
from app.engine.techcomm_threshold import techcomm_threshold
# ---------------------------------------------------------------------------
# WoT threshold: real-world vote Forgeron
# ---------------------------------------------------------------------------
class TestWotThresholdForgeron:
"""Test with the actual Engagement Forgeron v2.0.0 vote numbers."""
def test_forgeron_vote_passes(self):
"""97 votes_for out of 120 total (wot=7224) must pass."""
threshold = wot_threshold(
wot_size=7224,
total_votes=120,
majority_pct=50,
base_exponent=0.1,
gradient_exponent=0.2,
)
# With low participation (120/7224 ~ 1.66%), near-unanimity is required.
# The historical threshold was 94, and 97 >= 94.
assert 97 >= threshold
# The threshold should be high relative to total votes (inertia effect)
assert threshold > 60, f"Threshold {threshold} should be well above simple majority"
def test_forgeron_vote_threshold_value(self):
"""Verify the computed threshold is in a reasonable range."""
threshold = wot_threshold(
wot_size=7224,
total_votes=120,
majority_pct=50,
base_exponent=0.1,
gradient_exponent=0.2,
)
# At ~1.66% participation, inertia should push threshold close to 78-95%
# of total votes. The exact value depends on the formula.
assert 80 <= threshold <= 120
# ---------------------------------------------------------------------------
# WoT threshold: low participation
# ---------------------------------------------------------------------------
class TestWotThresholdLowParticipation:
"""With very low participation, near-unanimity should be required."""
def test_ten_votes_out_of_7224(self):
"""10 voters out of 7224 => nearly all must vote 'for'."""
threshold = wot_threshold(
wot_size=7224,
total_votes=10,
majority_pct=50,
base_exponent=0.1,
gradient_exponent=0.2,
)
# With participation ratio 10/7224 ~ 0.14%, threshold should be
# very close to total_votes (near-unanimity).
assert threshold >= 9, f"Expected near-unanimity but got threshold={threshold}"
assert threshold <= 10
# ---------------------------------------------------------------------------
# WoT threshold: high participation
# ---------------------------------------------------------------------------
class TestWotThresholdHighParticipation:
"""With high participation, threshold should approach simple majority M."""
def test_3000_votes_out_of_7224(self):
"""3000/7224 ~ 41.5% participation => threshold closer to 50%."""
threshold = wot_threshold(
wot_size=7224,
total_votes=3000,
majority_pct=50,
base_exponent=0.1,
gradient_exponent=0.2,
)
# With ~42% participation, the inertia factor diminishes.
# threshold should be well below 90% of votes but above simple majority.
simple_majority = math.ceil(3000 * 0.5)
assert threshold >= simple_majority, (
f"Threshold {threshold} should be at least simple majority {simple_majority}"
)
# Should be noticeably less than near-unanimity
assert threshold < 2700, (
f"Threshold {threshold} should be much less than near-unanimity at high participation"
)
# ---------------------------------------------------------------------------
# WoT threshold: edge cases
# ---------------------------------------------------------------------------
class TestWotThresholdEdgeCases:
"""Edge-case behaviour."""
def test_zero_total_votes(self):
"""With zero votes, threshold is ceil(C + B^W)."""
threshold = wot_threshold(
wot_size=7224,
total_votes=0,
majority_pct=50,
base_exponent=0.1,
gradient_exponent=0.2,
)
# B^W = 0.1^7224 is effectively 0
expected = math.ceil(0.0 + 0.1 ** 7224)
assert threshold == expected
def test_invalid_wot_size_zero(self):
with pytest.raises(ValueError, match="wot_size"):
wot_threshold(wot_size=0, total_votes=10)
def test_invalid_negative_votes(self):
with pytest.raises(ValueError, match="total_votes"):
wot_threshold(wot_size=100, total_votes=-1)
def test_invalid_majority_pct(self):
with pytest.raises(ValueError, match="majority_pct"):
wot_threshold(wot_size=100, total_votes=10, majority_pct=150)
# ---------------------------------------------------------------------------
# Smith threshold
# ---------------------------------------------------------------------------
class TestSmithThreshold:
"""Test Smith sub-WoT threshold: ceil(smith_size ^ S)."""
def test_smith_size_20_exponent_01(self):
"""smith_size=20, exponent=0.1 => ceil(20^0.1)."""
result = smith_threshold(smith_wot_size=20, exponent=0.1)
expected = math.ceil(20 ** 0.1)
assert result == expected
# 20^0.1 ~ 1.35, ceil => 2
assert result == 2
def test_smith_size_1(self):
"""smith_size=1 => ceil(1^0.1) = 1."""
assert smith_threshold(smith_wot_size=1, exponent=0.1) == 1
def test_smith_invalid(self):
with pytest.raises(ValueError):
smith_threshold(smith_wot_size=0)
# ---------------------------------------------------------------------------
# TechComm threshold
# ---------------------------------------------------------------------------
class TestTechcommThreshold:
"""Test TechComm threshold: ceil(cotec_size ^ T)."""
def test_cotec_size_5_exponent_01(self):
"""cotec_size=5, exponent=0.1 => ceil(5^0.1)."""
result = techcomm_threshold(cotec_size=5, exponent=0.1)
expected = math.ceil(5 ** 0.1)
assert result == expected
# 5^0.1 ~ 1.175, ceil => 2
assert result == 2
def test_cotec_size_1(self):
assert techcomm_threshold(cotec_size=1, exponent=0.1) == 1
def test_cotec_invalid(self):
with pytest.raises(ValueError):
techcomm_threshold(cotec_size=0)