gabriel / musehub public
test_graph_service.py python
218 lines 9.6 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 """TDD — IdentityGraphService integration tests.
2
3 All three invariants enforced atomically through one surface.
4 Mutations either fully commit or fully reject — no partial writes.
5 """
6 from __future__ import annotations
7
8 from decimal import Decimal
9
10 import pytest
11
12 from musehub.graph.dag import EdgeType, NodeType
13 from musehub.graph.cycle import CycleError
14 from musehub.graph.service import IdentityGraphService
15
16
17 # ── Fixtures ──────────────────────────────────────────────────────────────────
18
19 @pytest.fixture
20 def svc() -> IdentityGraphService:
21 return IdentityGraphService()
22
23
24 @pytest.fixture
25 def human_and_agent(svc: IdentityGraphService) -> IdentityGraphService:
26 svc.add_identity("alice", NodeType.HUMAN)
27 svc.add_identity("bot-1", NodeType.AGENT)
28 svc.add_spawn("alice", "bot-1")
29 return svc
30
31
32 @pytest.fixture
33 def two_orgs(svc: IdentityGraphService) -> IdentityGraphService:
34 svc.add_identity("alice", NodeType.HUMAN)
35 svc.add_identity("acme", NodeType.ORG, quorum=1)
36 svc.add_identity("lab", NodeType.ORG, quorum=1)
37 svc.add_membership("alice", "acme", weight=Decimal("1"))
38 return svc
39
40
41 # ── add_identity ──────────────────────────────────────────────────────────────
42
43 class TestAddIdentity:
44 def test_add_human(self, svc: IdentityGraphService) -> None:
45 svc.add_identity("alice", NodeType.HUMAN)
46 assert svc.node_type("alice") == NodeType.HUMAN
47
48 def test_add_agent(self, svc: IdentityGraphService) -> None:
49 svc.add_identity("bot-1", NodeType.AGENT)
50 assert svc.node_type("bot-1") == NodeType.AGENT
51
52 def test_add_org(self, svc: IdentityGraphService) -> None:
53 svc.add_identity("acme", NodeType.ORG, quorum=2)
54 assert svc.node_type("acme") == NodeType.ORG
55
56 def test_duplicate_handle_raises(self, svc: IdentityGraphService) -> None:
57 svc.add_identity("alice", NodeType.HUMAN)
58 with pytest.raises(ValueError, match="already exists"):
59 svc.add_identity("alice", NodeType.AGENT)
60
61 def test_unknown_handle_raises_on_lookup(self, svc: IdentityGraphService) -> None:
62 with pytest.raises(KeyError):
63 svc.node_type("ghost")
64
65
66 # ── add_spawn ─────────────────────────────────────────────────────────────────
67
68 class TestAddSpawn:
69 def test_human_spawns_agent(self, svc: IdentityGraphService) -> None:
70 svc.add_identity("alice", NodeType.HUMAN)
71 svc.add_identity("bot-1", NodeType.AGENT)
72 svc.add_spawn("alice", "bot-1")
73 assert svc.root_distance("bot-1") == 1
74
75 def test_agent_spawns_agent(self, human_and_agent: IdentityGraphService) -> None:
76 human_and_agent.add_identity("bot-2", NodeType.AGENT)
77 human_and_agent.add_spawn("bot-1", "bot-2")
78 assert human_and_agent.root_distance("bot-2") == 2
79
80 def test_spawn_cycle_rejected_atomically(self, human_and_agent: IdentityGraphService) -> None:
81 with pytest.raises(CycleError):
82 human_and_agent.add_spawn("bot-1", "alice")
83 # graph must be unchanged — alice still has depth 0
84 assert human_and_agent.root_distance("alice") == 0
85
86 def test_unknown_spawner_raises(self, svc: IdentityGraphService) -> None:
87 svc.add_identity("bot-1", NodeType.AGENT)
88 with pytest.raises(KeyError):
89 svc.add_spawn("nobody", "bot-1")
90
91 def test_unknown_spawnee_raises(self, svc: IdentityGraphService) -> None:
92 svc.add_identity("alice", NodeType.HUMAN)
93 with pytest.raises(KeyError):
94 svc.add_spawn("alice", "nobody")
95
96
97 # ── add_membership ────────────────────────────────────────────────────────────
98
99 class TestAddMembership:
100 def test_human_joins_org(self, svc: IdentityGraphService) -> None:
101 svc.add_identity("alice", NodeType.HUMAN)
102 svc.add_identity("acme", NodeType.ORG, quorum=1)
103 svc.add_membership("alice", "acme", weight=Decimal("1"))
104 assert svc.root_distance("acme") == 1
105
106 def test_org_joins_parent_org(self, two_orgs: IdentityGraphService) -> None:
107 two_orgs.add_membership("acme", "lab", weight=Decimal("1"))
108 assert two_orgs.root_distance("lab") == 2
109
110 def test_membership_cycle_rejected_atomically(self, two_orgs: IdentityGraphService) -> None:
111 two_orgs.add_membership("acme", "lab", weight=Decimal("1"))
112 with pytest.raises(CycleError):
113 two_orgs.add_membership("lab", "acme", weight=Decimal("1"))
114 # acme still has depth 1, not corrupted
115 assert two_orgs.root_distance("acme") == 1
116
117 def test_non_org_target_raises(self, svc: IdentityGraphService) -> None:
118 svc.add_identity("alice", NodeType.HUMAN)
119 svc.add_identity("bob", NodeType.HUMAN)
120 with pytest.raises(ValueError, match="not an org"):
121 svc.add_membership("alice", "bob", weight=Decimal("1"))
122
123
124 # ── root_distance ─────────────────────────────────────────────────────────────
125
126 class TestRootDistance:
127 def test_human_is_zero(self, svc: IdentityGraphService) -> None:
128 svc.add_identity("alice", NodeType.HUMAN)
129 assert svc.root_distance("alice") == 0
130
131 def test_orphan_agent_is_none(self, svc: IdentityGraphService) -> None:
132 svc.add_identity("bot-1", NodeType.AGENT)
133 assert svc.root_distance("bot-1") is None
134
135 def test_depth_updates_after_spawn(self, svc: IdentityGraphService) -> None:
136 svc.add_identity("alice", NodeType.HUMAN)
137 svc.add_identity("bot-1", NodeType.AGENT)
138 assert svc.root_distance("bot-1") is None
139 svc.add_spawn("alice", "bot-1")
140 assert svc.root_distance("bot-1") == 1
141
142 def test_depth_updates_cascade(self, svc: IdentityGraphService) -> None:
143 # add chain incrementally, verify depth updates at each step
144 svc.add_identity("h", NodeType.HUMAN)
145 svc.add_identity("a1", NodeType.AGENT)
146 svc.add_identity("a2", NodeType.AGENT)
147 svc.add_identity("a3", NodeType.AGENT)
148
149 svc.add_spawn("h", "a1")
150 assert svc.root_distance("a1") == 1
151 assert svc.root_distance("a2") is None
152
153 svc.add_spawn("a1", "a2")
154 assert svc.root_distance("a2") == 2
155
156 svc.add_spawn("a2", "a3")
157 assert svc.root_distance("a3") == 3
158
159
160 # ── is_quorum_met ─────────────────────────────────────────────────────────────
161
162 class TestServiceQuorum:
163 def test_simple_quorum_via_service(self, svc: IdentityGraphService) -> None:
164 svc.add_identity("alice", NodeType.HUMAN)
165 svc.add_identity("bob", NodeType.HUMAN)
166 svc.add_identity("acme", NodeType.ORG, quorum=2)
167 svc.add_membership("alice", "acme", weight=Decimal("1"))
168 svc.add_membership("bob", "acme", weight=Decimal("1"))
169
170 assert svc.is_quorum_met("acme", voters={"alice", "bob"}) is True
171 assert svc.is_quorum_met("acme", voters={"alice"}) is False
172
173 def test_nested_quorum_via_service(self, svc: IdentityGraphService) -> None:
174 svc.add_identity("alice", NodeType.HUMAN)
175 svc.add_identity("sub-org", NodeType.ORG, quorum=1)
176 svc.add_identity("parent", NodeType.ORG, quorum=2)
177 svc.add_membership("alice", "sub-org", weight=Decimal("1"))
178 svc.add_membership("sub-org", "parent", weight=Decimal("1"))
179 # need another direct member for parent quorum=2
180 svc.add_identity("bob", NodeType.HUMAN)
181 svc.add_membership("bob", "parent", weight=Decimal("1"))
182
183 # alice votes in sub-org → sub-org quorum met → sub-org vote in parent counts
184 # bob votes in parent directly → total = 2 → met
185 assert svc.is_quorum_met(
186 "parent",
187 voters={"alice", "sub-org", "bob"},
188 sub_votes={"sub-org": {"alice"}},
189 ) is True
190
191 # sub-org votes in parent but nobody voted inside it → doesn't count
192 assert svc.is_quorum_met(
193 "parent",
194 voters={"sub-org", "bob"},
195 sub_votes={},
196 ) is False
197
198
199 # ── human_ancestors ───────────────────────────────────────────────────────────
200
201 class TestServiceAncestors:
202 def test_agent_inherits_human_spawner(self, svc: IdentityGraphService) -> None:
203 svc.add_identity("alice", NodeType.HUMAN)
204 svc.add_identity("bot", NodeType.AGENT)
205 svc.add_spawn("alice", "bot")
206 assert svc.human_ancestors("bot") == {"alice"}
207
208 def test_org_inherits_all_human_members(self, svc: IdentityGraphService) -> None:
209 svc.add_identity("alice", NodeType.HUMAN)
210 svc.add_identity("bob", NodeType.HUMAN)
211 svc.add_identity("acme", NodeType.ORG, quorum=1)
212 svc.add_membership("alice", "acme", weight=Decimal("1"))
213 svc.add_membership("bob", "acme", weight=Decimal("1"))
214 assert svc.human_ancestors("acme") == {"alice", "bob"}
215
216 def test_no_ancestors_is_empty_set(self, svc: IdentityGraphService) -> None:
217 svc.add_identity("bot", NodeType.AGENT)
218 assert svc.human_ancestors("bot") == set()
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago