gabriel / musehub public
test_graph_quorum.py python
234 lines 9.6 KB
Raw
sha256:4461d2674257c5b59a460e0ec23d3680462662e9837ef25b675489d2e42cb994 fix: resolve "I3" naming collision between push_validator.p… Sonnet 5 minor ⚠ breaking 2 days ago
1 """TDD — recursive org-vote quorum tallying (musehub#227: not "I3" — see
2 musehub/graph/quorum.py's module docstring for why).
3
4 An org's vote in a parent org counts only if that org's own quorum
5 is independently satisfied. Recursive. Terminates because I1 guarantees
6 the graph is a DAG.
7 """
8 from __future__ import annotations
9
10 from collections.abc import Mapping
11 from decimal import Decimal
12
13 import pytest
14
15 from musehub.graph.dag import EdgeType, GraphEdge, IdentityDAG, NodeType
16 from musehub.graph.quorum import OrgSpec, QuorumEngine, VoteRecord
17
18
19 S = EdgeType.SPAWNS
20 M = EdgeType.MEMBER_OF
21
22
23 def spec(handle: str, quorum: int, members: Mapping[str, Decimal]) -> OrgSpec:
24 """Convenience constructor for OrgSpec."""
25 return OrgSpec(handle=handle, quorum=quorum, member_weights=members)
26
27
28 def vote(voter: str, org: str) -> VoteRecord:
29 return VoteRecord(voter_handle=voter, org_handle=org)
30
31
32 # ── Simple flat orgs ──────────────────────────────────────────────────────────
33
34 class TestFlatQuorum:
35 def test_quorum_met_exact_threshold(self) -> None:
36 # 3 members, quorum=2, exactly 2 vote
37 engine = QuorumEngine(
38 orgs={"acme": spec("acme", quorum=2, members={
39 "alice": Decimal("1"),
40 "bob": Decimal("1"),
41 "carol": Decimal("1"),
42 })}
43 )
44 votes = [vote("alice", "acme"), vote("bob", "acme")]
45 assert engine.is_quorum_met("acme", votes) is True
46
47 def test_quorum_not_met_one_short(self) -> None:
48 engine = QuorumEngine(
49 orgs={"acme": spec("acme", quorum=2, members={
50 "alice": Decimal("1"),
51 "bob": Decimal("1"),
52 "carol": Decimal("1"),
53 })}
54 )
55 votes = [vote("alice", "acme")]
56 assert engine.is_quorum_met("acme", votes) is False
57
58 def test_quorum_not_met_no_votes(self) -> None:
59 engine = QuorumEngine(
60 orgs={"acme": spec("acme", quorum=1, members={"alice": Decimal("1")})}
61 )
62 assert engine.is_quorum_met("acme", []) is False
63
64 def test_quorum_1_met_by_single_vote(self) -> None:
65 engine = QuorumEngine(
66 orgs={"acme": spec("acme", quorum=1, members={"alice": Decimal("1")})}
67 )
68 assert engine.is_quorum_met("acme", [vote("alice", "acme")]) is True
69
70 def test_unanimous_quorum(self) -> None:
71 engine = QuorumEngine(
72 orgs={"acme": spec("acme", quorum=3, members={
73 "alice": Decimal("1"),
74 "bob": Decimal("1"),
75 "carol": Decimal("1"),
76 })}
77 )
78 votes = [vote("alice", "acme"), vote("bob", "acme"), vote("carol", "acme")]
79 assert engine.is_quorum_met("acme", votes) is True
80
81 def test_non_member_vote_does_not_count(self) -> None:
82 engine = QuorumEngine(
83 orgs={"acme": spec("acme", quorum=1, members={"alice": Decimal("1")})}
84 )
85 # dave is not a member of acme
86 assert engine.is_quorum_met("acme", [vote("dave", "acme")]) is False
87
88
89 # ── Weighted members ──────────────────────────────────────────────────────────
90
91 class TestWeightedQuorum:
92 def test_weighted_vote_reaches_quorum(self) -> None:
93 # alice has weight 2, quorum=2 → alice alone satisfies it
94 engine = QuorumEngine(
95 orgs={"acme": spec("acme", quorum=2, members={
96 "alice": Decimal("2"),
97 "bob": Decimal("1"),
98 })}
99 )
100 assert engine.is_quorum_met("acme", [vote("alice", "acme")]) is True
101
102 def test_low_weight_vote_does_not_reach_quorum(self) -> None:
103 engine = QuorumEngine(
104 orgs={"acme": spec("acme", quorum=3, members={
105 "alice": Decimal("1"),
106 "bob": Decimal("1"),
107 "carol": Decimal("1"),
108 })}
109 )
110 # only alice (weight 1) voted — needs 3
111 assert engine.is_quorum_met("acme", [vote("alice", "acme")]) is False
112
113 def test_fractional_weights_sum_to_quorum(self) -> None:
114 engine = QuorumEngine(
115 orgs={"acme": spec("acme", quorum=1, members={
116 "alice": Decimal("0.5"),
117 "bob": Decimal("0.5"),
118 })}
119 )
120 votes = [vote("alice", "acme"), vote("bob", "acme")]
121 assert engine.is_quorum_met("acme", votes) is True
122
123
124 # ── Nested orgs — quorum propagation ─────────────────────────────────────────
125
126 class TestNestedQuorum:
127 def _two_level_engine(self) -> QuorumEngine:
128 # parent-org has [alice, sub-org] as members, quorum=2
129 # sub-org has [bob, carol] as members, quorum=1
130 return QuorumEngine(orgs={
131 "parent-org": spec("parent-org", quorum=2, members={
132 "alice": Decimal("1"),
133 "sub-org": Decimal("1"),
134 }),
135 "sub-org": spec("sub-org", quorum=1, members={
136 "bob": Decimal("1"),
137 "carol": Decimal("1"),
138 }),
139 })
140
141 def test_org_vote_counts_when_its_quorum_met(self) -> None:
142 engine = self._two_level_engine()
143 # alice votes in parent; bob votes in sub-org (satisfying sub-org quorum=1)
144 # sub-org's vote in parent then counts → parent total = 2 → met
145 votes = [
146 vote("alice", "parent-org"),
147 vote("bob", "sub-org"),
148 vote("sub-org", "parent-org"),
149 ]
150 assert engine.is_quorum_met("parent-org", votes) is True
151
152 def test_org_vote_does_not_count_when_its_quorum_not_met(self) -> None:
153 engine = self._two_level_engine()
154 # sub-org votes in parent, but nobody voted inside sub-org → sub-org quorum not met
155 votes = [
156 vote("alice", "parent-org"),
157 vote("sub-org", "parent-org"),
158 ]
159 assert engine.is_quorum_met("parent-org", votes) is False
160
161 def test_parent_quorum_not_met_even_if_sub_quorum_met(self) -> None:
162 engine = self._two_level_engine()
163 # sub-org quorum met (bob voted) but only sub-org votes in parent → total=1 < quorum=2
164 votes = [
165 vote("bob", "sub-org"),
166 vote("sub-org", "parent-org"),
167 ]
168 assert engine.is_quorum_met("parent-org", votes) is False
169
170 def test_three_level_nesting_all_quorums_met(self) -> None:
171 engine = QuorumEngine(orgs={
172 "top": spec("top", quorum=2, members={"alice": Decimal("1"), "mid": Decimal("1")}),
173 "mid": spec("mid", quorum=1, members={"bob": Decimal("1"), "bot": Decimal("1")}),
174 "bot": spec("bot", quorum=1, members={"carol": Decimal("1")}),
175 })
176 # carol votes in bot → bot quorum met
177 # bot votes in mid → mid quorum met
178 # alice + mid vote in top → top quorum met
179 votes = [
180 vote("carol", "bot"),
181 vote("bot", "mid"),
182 vote("bob", "mid"), # extra, doesn't hurt
183 vote("mid", "top"),
184 vote("alice", "top"),
185 ]
186 assert engine.is_quorum_met("top", votes) is True
187
188 def test_three_level_nesting_middle_quorum_not_met(self) -> None:
189 engine = QuorumEngine(orgs={
190 "top": spec("top", quorum=2, members={"alice": Decimal("1"), "mid": Decimal("1")}),
191 "mid": spec("mid", quorum=2, members={"bob": Decimal("1"), "carol": Decimal("1")}),
192 })
193 # only bob voted in mid → mid quorum (needs 2) not met → mid vote in top doesn't count
194 votes = [
195 vote("bob", "mid"),
196 vote("mid", "top"),
197 vote("alice", "top"),
198 ]
199 assert engine.is_quorum_met("top", votes) is False
200
201
202 # ── Effective weight ──────────────────────────────────────────────────────────
203
204 class TestEffectiveWeight:
205 def test_human_effective_weight_is_direct(self) -> None:
206 engine = QuorumEngine(orgs={
207 "acme": spec("acme", quorum=1, members={"alice": Decimal("2")})
208 })
209 assert engine.effective_weight("acme", "alice", []) == Decimal("2")
210
211 def test_org_effective_weight_zero_when_quorum_not_met(self) -> None:
212 engine = QuorumEngine(orgs={
213 "parent": spec("parent", quorum=1, members={"sub": Decimal("3")}),
214 "sub": spec("sub", quorum=1, members={"alice": Decimal("1")}),
215 })
216 # sub's quorum not met (alice hasn't voted in sub) → sub weight in parent = 0
217 assert engine.effective_weight("parent", "sub", []) == Decimal("0")
218
219 def test_org_effective_weight_is_direct_when_quorum_met(self) -> None:
220 engine = QuorumEngine(orgs={
221 "parent": spec("parent", quorum=1, members={"sub": Decimal("3")}),
222 "sub": spec("sub", quorum=1, members={"alice": Decimal("1")}),
223 })
224 votes = [vote("alice", "sub"), vote("sub", "parent")]
225 assert engine.effective_weight("parent", "sub", votes) == Decimal("3")
226
227
228 # ── Unknown org ───────────────────────────────────────────────────────────────
229
230 class TestUnknownOrg:
231 def test_unknown_org_raises(self) -> None:
232 engine = QuorumEngine(orgs={})
233 with pytest.raises(KeyError):
234 engine.is_quorum_met("ghost-org", [])
File History 1 commit
sha256:4461d2674257c5b59a460e0ec23d3680462662e9837ef25b675489d2e42cb994 fix: resolve "I3" naming collision between push_validator.p… Sonnet 5 minor ⚠ 2 days ago