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:
180
backend/app/tests/test_threshold.py
Normal file
180
backend/app/tests/test_threshold.py
Normal 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)
|
||||
Reference in New Issue
Block a user