"""TDD — IdentityGraphService integration tests. All three invariants enforced atomically through one surface. Mutations either fully commit or fully reject — no partial writes. """ from __future__ import annotations from decimal import Decimal import pytest from musehub.graph.dag import EdgeType, NodeType from musehub.graph.cycle import CycleError from musehub.graph.service import IdentityGraphService # ── Fixtures ────────────────────────────────────────────────────────────────── @pytest.fixture def svc() -> IdentityGraphService: return IdentityGraphService() @pytest.fixture def human_and_agent(svc: IdentityGraphService) -> IdentityGraphService: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("bot-1", NodeType.AGENT) svc.add_spawn("alice", "bot-1") return svc @pytest.fixture def two_orgs(svc: IdentityGraphService) -> IdentityGraphService: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("acme", NodeType.ORG, quorum=1) svc.add_identity("lab", NodeType.ORG, quorum=1) svc.add_membership("alice", "acme", weight=Decimal("1")) return svc # ── add_identity ────────────────────────────────────────────────────────────── class TestAddIdentity: def test_add_human(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) assert svc.node_type("alice") == NodeType.HUMAN def test_add_agent(self, svc: IdentityGraphService) -> None: svc.add_identity("bot-1", NodeType.AGENT) assert svc.node_type("bot-1") == NodeType.AGENT def test_add_org(self, svc: IdentityGraphService) -> None: svc.add_identity("acme", NodeType.ORG, quorum=2) assert svc.node_type("acme") == NodeType.ORG def test_duplicate_handle_raises(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) with pytest.raises(ValueError, match="already exists"): svc.add_identity("alice", NodeType.AGENT) def test_unknown_handle_raises_on_lookup(self, svc: IdentityGraphService) -> None: with pytest.raises(KeyError): svc.node_type("ghost") # ── add_spawn ───────────────────────────────────────────────────────────────── class TestAddSpawn: def test_human_spawns_agent(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("bot-1", NodeType.AGENT) svc.add_spawn("alice", "bot-1") assert svc.root_distance("bot-1") == 1 def test_agent_spawns_agent(self, human_and_agent: IdentityGraphService) -> None: human_and_agent.add_identity("bot-2", NodeType.AGENT) human_and_agent.add_spawn("bot-1", "bot-2") assert human_and_agent.root_distance("bot-2") == 2 def test_spawn_cycle_rejected_atomically(self, human_and_agent: IdentityGraphService) -> None: with pytest.raises(CycleError): human_and_agent.add_spawn("bot-1", "alice") # graph must be unchanged — alice still has depth 0 assert human_and_agent.root_distance("alice") == 0 def test_unknown_spawner_raises(self, svc: IdentityGraphService) -> None: svc.add_identity("bot-1", NodeType.AGENT) with pytest.raises(KeyError): svc.add_spawn("nobody", "bot-1") def test_unknown_spawnee_raises(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) with pytest.raises(KeyError): svc.add_spawn("alice", "nobody") # ── add_membership ──────────────────────────────────────────────────────────── class TestAddMembership: def test_human_joins_org(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("acme", NodeType.ORG, quorum=1) svc.add_membership("alice", "acme", weight=Decimal("1")) assert svc.root_distance("acme") == 1 def test_org_joins_parent_org(self, two_orgs: IdentityGraphService) -> None: two_orgs.add_membership("acme", "lab", weight=Decimal("1")) assert two_orgs.root_distance("lab") == 2 def test_membership_cycle_rejected_atomically(self, two_orgs: IdentityGraphService) -> None: two_orgs.add_membership("acme", "lab", weight=Decimal("1")) with pytest.raises(CycleError): two_orgs.add_membership("lab", "acme", weight=Decimal("1")) # acme still has depth 1, not corrupted assert two_orgs.root_distance("acme") == 1 def test_non_org_target_raises(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("bob", NodeType.HUMAN) with pytest.raises(ValueError, match="not an org"): svc.add_membership("alice", "bob", weight=Decimal("1")) # ── root_distance ───────────────────────────────────────────────────────────── class TestRootDistance: def test_human_is_zero(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) assert svc.root_distance("alice") == 0 def test_orphan_agent_is_none(self, svc: IdentityGraphService) -> None: svc.add_identity("bot-1", NodeType.AGENT) assert svc.root_distance("bot-1") is None def test_depth_updates_after_spawn(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("bot-1", NodeType.AGENT) assert svc.root_distance("bot-1") is None svc.add_spawn("alice", "bot-1") assert svc.root_distance("bot-1") == 1 def test_depth_updates_cascade(self, svc: IdentityGraphService) -> None: # add chain incrementally, verify depth updates at each step svc.add_identity("h", NodeType.HUMAN) svc.add_identity("a1", NodeType.AGENT) svc.add_identity("a2", NodeType.AGENT) svc.add_identity("a3", NodeType.AGENT) svc.add_spawn("h", "a1") assert svc.root_distance("a1") == 1 assert svc.root_distance("a2") is None svc.add_spawn("a1", "a2") assert svc.root_distance("a2") == 2 svc.add_spawn("a2", "a3") assert svc.root_distance("a3") == 3 # ── is_quorum_met ───────────────────────────────────────────────────────────── class TestServiceQuorum: def test_simple_quorum_via_service(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("bob", NodeType.HUMAN) svc.add_identity("acme", NodeType.ORG, quorum=2) svc.add_membership("alice", "acme", weight=Decimal("1")) svc.add_membership("bob", "acme", weight=Decimal("1")) assert svc.is_quorum_met("acme", voters={"alice", "bob"}) is True assert svc.is_quorum_met("acme", voters={"alice"}) is False def test_nested_quorum_via_service(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("sub-org", NodeType.ORG, quorum=1) svc.add_identity("parent", NodeType.ORG, quorum=2) svc.add_membership("alice", "sub-org", weight=Decimal("1")) svc.add_membership("sub-org", "parent", weight=Decimal("1")) # need another direct member for parent quorum=2 svc.add_identity("bob", NodeType.HUMAN) svc.add_membership("bob", "parent", weight=Decimal("1")) # alice votes in sub-org → sub-org quorum met → sub-org vote in parent counts # bob votes in parent directly → total = 2 → met assert svc.is_quorum_met( "parent", voters={"alice", "sub-org", "bob"}, sub_votes={"sub-org": {"alice"}}, ) is True # sub-org votes in parent but nobody voted inside it → doesn't count assert svc.is_quorum_met( "parent", voters={"sub-org", "bob"}, sub_votes={}, ) is False # ── human_ancestors ─────────────────────────────────────────────────────────── class TestServiceAncestors: def test_agent_inherits_human_spawner(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("bot", NodeType.AGENT) svc.add_spawn("alice", "bot") assert svc.human_ancestors("bot") == {"alice"} def test_org_inherits_all_human_members(self, svc: IdentityGraphService) -> None: svc.add_identity("alice", NodeType.HUMAN) svc.add_identity("bob", NodeType.HUMAN) svc.add_identity("acme", NodeType.ORG, quorum=1) svc.add_membership("alice", "acme", weight=Decimal("1")) svc.add_membership("bob", "acme", weight=Decimal("1")) assert svc.human_ancestors("acme") == {"alice", "bob"} def test_no_ancestors_is_empty_set(self, svc: IdentityGraphService) -> None: svc.add_identity("bot", NodeType.AGENT) assert svc.human_ancestors("bot") == set()