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