"""TDD tests for Phase 5 MCP profile + attestation + MPay executor functions. Tests are written RED-first against the public contracts described in issue #2. They drive the implementation of six new executor functions and their dispatcher routing. Covered executors: 1. execute_read_profile_manifest — full archetype-aware manifest 2. execute_issue_attestation — verify sig + persist 3. execute_revoke_attestation — revoke by id, attester-only 4. execute_list_attestations — query by subject 5. execute_record_mpay_claim — verify sig + persist, idempotent 6. execute_get_mpay_ledger — sent + received totals """ from __future__ import annotations from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from muse.core.types import encode_pubkey, encode_sig, long_id # --------------------------------------------------------------------------- # Helpers — shared across all test groups # --------------------------------------------------------------------------- def _make_ed25519_pair() -> tuple[Ed25519PrivateKey, str]: privkey = Ed25519PrivateKey.generate() pub_bytes = privkey.public_key().public_bytes_raw() return privkey, encode_pubkey("ed25519", pub_bytes) def _sign_attest(privkey: Ed25519PrivateKey, attester: str, subject: str, claim: str, ts: str) -> str: msg = f"ATTEST\n{attester}\n{subject}\n{claim}\n{ts}".encode() return encode_sig("ed25519", privkey.sign(msg)) def _sign_mpay(privkey: Ed25519PrivateKey, sender: str, recipient: str, amount: int, nonce: str) -> str: msg = f"MPAY\n{sender}\n{recipient}\n{amount}\n{nonce}".encode() return encode_sig("ed25519", privkey.sign(msg)) _ATTESTER = "gabriel" _SUBJECT = "aria" _CLAIM = '{"type": "human", "confidence": 0.99}' _TS = "2026-04-21T12:00:00+00:00" _SENDER = "gabriel" _RECIPIENT = "aria" _AMOUNT_NANO = 500_000 _NONCE_HEX = "b" * 64 # --------------------------------------------------------------------------- # 1. execute_read_profile_manifest # --------------------------------------------------------------------------- class TestExecuteReadProfileManifest: """execute_read_profile_manifest must return the full archetype-aware manifest, not just basic bio/avatar fields.""" def _make_manifest(self, identity_type: str = "human") -> MagicMock: from musehub.models.musehub import ( ActivityDomain, AttestationBadge, OrgManifest, ProfileManifest, ProfileRepoSummary, TrustChainEntry, ) now = datetime.now(timezone.utc) return ProfileManifest( identity_id=long_id("a" * 64), handle="gabriel", identity_type=identity_type, display_name="Carlos Gabriel Cardona", bio="Building the sound of the future", avatar_url="https://staging.musehub.ai/avatars/gabriel.png", location="San Francisco", website_url="https://gabriel.dev", social_url="https://x.com/gabriel", is_verified=True, pinned_repo_ids=[], repos=[], created_at=now, updated_at=now, activity=[ ActivityDomain(domain="code", grid=[0] * 364, peak=0, total=0), ActivityDomain(domain="music", grid=[3] * 364, peak=3, total=3 * 364), ], attestations=[ AttestationBadge( attestation_id=long_id("b" * 64), attester="aaronrene", subject="gabriel", claim_type="collab", claim='{"type":"collab"}', issued_at=now, ) ], avax_address="0x1a2b3c4d5e6f" if identity_type == "human" else None, agent_model="claude-sonnet-4-6" if identity_type == "agent" else None, agent_capabilities=["read:repos", "write:repos"] if identity_type == "agent" else [], trust_chain=[TrustChainEntry(handle="gabriel", identity_type="human")] if identity_type == "agent" else [], org=OrgManifest(members=["gabriel", "aria"], quorum=2) if identity_type == "org" else None, mpay_total_sent_nano=1_000_000, mpay_total_received_nano=500_000, ) @pytest.mark.asyncio async def test_returns_full_manifest_fields(self) -> None: """Result data must include identity_type, activity, attestations, avax_address, mpay totals.""" from musehub.services.musehub_mcp_executor import execute_read_profile_manifest manifest = self._make_manifest("human") with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_profile.build_profile_manifest", new=AsyncMock(return_value=manifest)), patch("musehub.services.musehub_attestations.get_attestations_for_subject", new=AsyncMock()), patch("musehub.services.musehub_attestations.attestation_to_badge", return_value=manifest.attestations[0]), patch("musehub.services.musehub_mpay.get_mpay_ledger", new=AsyncMock()), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_read_profile_manifest(handle="gabriel") assert result.ok is True d = result.data assert d is not None assert d["handle"] == "gabriel" assert d["identity_type"] == "human" assert "activity" in d assert "attestations" in d assert d["avax_address"] == "0x1a2b3c4d5e6f" assert d["mpay_total_sent_nano"] == 1_000_000 assert d["mpay_total_received_nano"] == 500_000 @pytest.mark.asyncio async def test_returns_agent_specific_fields(self) -> None: """Agent manifest includes agent_model, agent_capabilities, trust_chain.""" from musehub.services.musehub_mcp_executor import execute_read_profile_manifest manifest = self._make_manifest("agent") with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_profile.build_profile_manifest", new=AsyncMock(return_value=manifest)), patch("musehub.services.musehub_attestations.get_attestations_for_subject", new=AsyncMock()), patch("musehub.services.musehub_attestations.attestation_to_badge", return_value=manifest.attestations[0]), patch("musehub.services.musehub_mpay.get_mpay_ledger", new=AsyncMock()), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_read_profile_manifest(handle="mix-engine-7") assert result.ok is True d = result.data assert d["identity_type"] == "agent" assert d["agent_model"] == "claude-sonnet-4-6" assert "read:repos" in d["agent_capabilities"] assert len(d["trust_chain"]) == 1 @pytest.mark.asyncio async def test_returns_org_specific_fields(self) -> None: """Org manifest includes org manifest with members and quorum.""" from musehub.services.musehub_mcp_executor import execute_read_profile_manifest manifest = self._make_manifest("org") with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_profile.build_profile_manifest", new=AsyncMock(return_value=manifest)), patch("musehub.services.musehub_attestations.get_attestations_for_subject", new=AsyncMock()), patch("musehub.services.musehub_attestations.attestation_to_badge", return_value=manifest.attestations[0]), patch("musehub.services.musehub_mpay.get_mpay_ledger", new=AsyncMock()), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_read_profile_manifest(handle="darkroom-collective") assert result.ok is True d = result.data assert d["identity_type"] == "org" assert d["org"] is not None assert "gabriel" in d["org"]["members"] assert d["org"]["quorum"] == 2 @pytest.mark.asyncio async def test_not_found_returns_error(self) -> None: from musehub.services.musehub_mcp_executor import execute_read_profile_manifest with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_profile.build_profile_manifest", new=AsyncMock(return_value=None)), patch("musehub.services.musehub_attestations.get_attestations_for_subject", new=AsyncMock()), patch("musehub.services.musehub_mpay.get_mpay_ledger", new=AsyncMock()), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_read_profile_manifest(handle="nobody") assert result.ok is False assert result.error_code == "profile_not_found" # --------------------------------------------------------------------------- # 2. execute_issue_attestation # --------------------------------------------------------------------------- class TestExecuteIssueAttestation: """execute_issue_attestation must verify the Ed25519 signature and persist.""" @pytest.mark.asyncio async def test_valid_attestation_returns_ok(self) -> None: from musehub.services.musehub_mcp_executor import execute_issue_attestation privkey, pubkey = _make_ed25519_pair() sig = _sign_attest(privkey, _ATTESTER, _SUBJECT, _CLAIM, _TS) issued_at = datetime.fromisoformat(_TS) mock_resp = MagicMock() mock_resp.attestation_id = long_id("c" * 64) mock_resp.attester = _ATTESTER mock_resp.subject = _SUBJECT mock_resp.claim = _CLAIM mock_resp.issued_at = issued_at mock_resp.revoked_at = None with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_attestations.issue_attestation", new=AsyncMock(return_value=mock_resp)), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_issue_attestation( attester=_ATTESTER, subject=_SUBJECT, claim=_CLAIM, issued_at_iso=_TS, signature=sig, attester_public_key=pubkey, ) assert result.ok is True d = result.data assert d["attester"] == _ATTESTER assert d["subject"] == _SUBJECT assert d["attestation_id"].startswith("sha256:") assert d["revoked_at"] is None @pytest.mark.asyncio async def test_invalid_signature_returns_error(self) -> None: from musehub.services.musehub_mcp_executor import execute_issue_attestation _, pubkey = _make_ed25519_pair() bad_sig = f"ed25519:{"Z" * 88}" # garbage with patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None): result = await execute_issue_attestation( attester=_ATTESTER, subject=_SUBJECT, claim=_CLAIM, issued_at_iso=_TS, signature=bad_sig, attester_public_key=pubkey, ) assert result.ok is False assert result.error_code == "invalid_attestation_signature" @pytest.mark.asyncio async def test_wrong_key_returns_error(self) -> None: """Signing key doesn't match supplied public key → verification fails.""" from musehub.services.musehub_mcp_executor import execute_issue_attestation privkey, _ = _make_ed25519_pair() _, other_pubkey = _make_ed25519_pair() sig = _sign_attest(privkey, _ATTESTER, _SUBJECT, _CLAIM, _TS) with patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None): result = await execute_issue_attestation( attester=_ATTESTER, subject=_SUBJECT, claim=_CLAIM, issued_at_iso=_TS, signature=sig, attester_public_key=other_pubkey, ) assert result.ok is False assert result.error_code == "invalid_attestation_signature" # --------------------------------------------------------------------------- # 3. execute_revoke_attestation # --------------------------------------------------------------------------- class TestExecuteRevokeAttestation: @pytest.mark.asyncio async def test_revoke_returns_revoked_at(self) -> None: from musehub.services.musehub_mcp_executor import execute_revoke_attestation now = datetime.now(timezone.utc) mock_resp = MagicMock() mock_resp.attestation_id = long_id("d" * 64) mock_resp.attester = _ATTESTER mock_resp.subject = _SUBJECT mock_resp.claim = _CLAIM mock_resp.issued_at = datetime.fromisoformat(_TS) mock_resp.revoked_at = now with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_attestations.revoke_attestation", new=AsyncMock(return_value=mock_resp)), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_revoke_attestation( attestation_id=long_id("d" * 64), revoker=_ATTESTER, ) assert result.ok is True assert result.data["revoked_at"] is not None @pytest.mark.asyncio async def test_not_found_returns_error(self) -> None: from musehub.services.musehub_mcp_executor import execute_revoke_attestation with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch( "musehub.services.musehub_attestations.revoke_attestation", new=AsyncMock(side_effect=KeyError("not found")), ), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_revoke_attestation( attestation_id=long_id("e" * 64), revoker=_ATTESTER, ) assert result.ok is False assert result.error_code == "attestation_not_found" @pytest.mark.asyncio async def test_wrong_revoker_returns_forbidden(self) -> None: from musehub.services.musehub_mcp_executor import execute_revoke_attestation with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch( "musehub.services.musehub_attestations.revoke_attestation", new=AsyncMock(side_effect=PermissionError("not your attestation")), ), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_revoke_attestation( attestation_id=long_id("d" * 64), revoker="impostor", ) assert result.ok is False assert result.error_code == "forbidden" # --------------------------------------------------------------------------- # 4. execute_list_attestations # --------------------------------------------------------------------------- class TestExecuteListAttestations: @pytest.mark.asyncio async def test_returns_list_with_count(self) -> None: from musehub.services.musehub_mcp_executor import execute_list_attestations from musehub.models.musehub import AttestationListResponse, AttestationResponse now = datetime.now(timezone.utc) badge = AttestationResponse( attestation_id=long_id("f" * 64), attester=_ATTESTER, subject=_SUBJECT, claim=_CLAIM, signature="ed25519:abc", attester_public_key="ed25519:def", issued_at=now, revoked_at=None, ) mock_resp = AttestationListResponse(subject=_SUBJECT, attestations=[badge], total=1) with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch( "musehub.services.musehub_attestations.get_attestations_for_subject", new=AsyncMock(return_value=mock_resp), ), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_list_attestations(subject=_SUBJECT, include_revoked=False) assert result.ok is True assert result.data["subject"] == _SUBJECT assert result.data["total"] == 1 assert len(result.data["attestations"]) == 1 assert result.data["attestations"][0]["attester"] == _ATTESTER @pytest.mark.asyncio async def test_empty_subject_returns_empty_list(self) -> None: from musehub.services.musehub_mcp_executor import execute_list_attestations from musehub.models.musehub import AttestationListResponse mock_resp = AttestationListResponse(subject="nobody", attestations=[], total=0) with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch( "musehub.services.musehub_attestations.get_attestations_for_subject", new=AsyncMock(return_value=mock_resp), ), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_list_attestations(subject="nobody") assert result.ok is True assert result.data["total"] == 0 assert result.data["attestations"] == [] # --------------------------------------------------------------------------- # 5. execute_record_mpay_claim # --------------------------------------------------------------------------- class TestExecuteRecordMpayClaim: @pytest.mark.asyncio async def test_valid_claim_returns_claim_id(self) -> None: from musehub.services.musehub_mcp_executor import execute_record_mpay_claim privkey, pubkey = _make_ed25519_pair() sig = _sign_mpay(privkey, _SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX) now = datetime.now(timezone.utc) mock_resp = MagicMock() mock_resp.claim_id = long_id("a" * 64) mock_resp.sender = _SENDER mock_resp.recipient = _RECIPIENT mock_resp.amount_nano = _AMOUNT_NANO mock_resp.nonce_hex = _NONCE_HEX mock_resp.created_at = now mock_resp.confirmed_at = None mock_resp.voided_at = None mock_resp.memo = None with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_mpay.record_mpay_claim", new=AsyncMock(return_value=mock_resp)), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_record_mpay_claim( sender=_SENDER, recipient=_RECIPIENT, amount_nano=_AMOUNT_NANO, nonce_hex=_NONCE_HEX, signature=sig, sender_public_key=pubkey, ) assert result.ok is True d = result.data assert d["sender"] == _SENDER assert d["recipient"] == _RECIPIENT assert d["amount_nano"] == _AMOUNT_NANO assert d["claim_id"].startswith("sha256:") @pytest.mark.asyncio async def test_invalid_mpay_signature_returns_error(self) -> None: from musehub.services.musehub_mcp_executor import execute_record_mpay_claim _, pubkey = _make_ed25519_pair() bad_sig = f"ed25519:{"Z" * 88}" with patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None): result = await execute_record_mpay_claim( sender=_SENDER, recipient=_RECIPIENT, amount_nano=_AMOUNT_NANO, nonce_hex=_NONCE_HEX, signature=bad_sig, sender_public_key=pubkey, ) assert result.ok is False assert result.error_code == "invalid_mpay_signature" @pytest.mark.asyncio async def test_zero_amount_returns_error(self) -> None: """Amount must be > 0.""" from musehub.services.musehub_mcp_executor import execute_record_mpay_claim privkey, pubkey = _make_ed25519_pair() sig = _sign_mpay(privkey, _SENDER, _RECIPIENT, 0, _NONCE_HEX) with patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None): result = await execute_record_mpay_claim( sender=_SENDER, recipient=_RECIPIENT, amount_nano=0, nonce_hex=_NONCE_HEX, signature=sig, sender_public_key=pubkey, ) assert result.ok is False assert result.error_code == "invalid_amount" @pytest.mark.asyncio async def test_self_payment_returns_error(self) -> None: """Sender and recipient must be different handles.""" from musehub.services.musehub_mcp_executor import execute_record_mpay_claim privkey, pubkey = _make_ed25519_pair() sig = _sign_mpay(privkey, _SENDER, _SENDER, _AMOUNT_NANO, _NONCE_HEX) with patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None): result = await execute_record_mpay_claim( sender=_SENDER, recipient=_SENDER, amount_nano=_AMOUNT_NANO, nonce_hex=_NONCE_HEX, signature=sig, sender_public_key=pubkey, ) assert result.ok is False assert result.error_code == "self_payment" # --------------------------------------------------------------------------- # 6. execute_get_mpay_ledger # --------------------------------------------------------------------------- class TestExecuteGetMpayLedger: @pytest.mark.asyncio async def test_returns_sent_received_totals(self) -> None: from musehub.services.musehub_mcp_executor import execute_get_mpay_ledger from musehub.models.musehub import MPayLedgerResponse, MPayClaimResponse now = datetime.now(timezone.utc) claim = MPayClaimResponse( claim_id=long_id("a" * 64), sender=_SENDER, recipient=_RECIPIENT, amount_nano=_AMOUNT_NANO, nonce_hex=_NONCE_HEX, signature="ed25519:abc", sender_public_key="ed25519:def", memo=None, created_at=now, confirmed_at=None, voided_at=None, ) mock_ledger = MPayLedgerResponse( handle=_SENDER, sent=[claim], received=[], total_sent_nano=_AMOUNT_NANO, total_received_nano=0, ) with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_mpay.get_mpay_ledger", new=AsyncMock(return_value=mock_ledger)), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session result = await execute_get_mpay_ledger(handle=_SENDER, limit=50) assert result.ok is True d = result.data assert d["handle"] == _SENDER assert d["total_sent_nano"] == _AMOUNT_NANO assert d["total_received_nano"] == 0 assert len(d["sent"]) == 1 assert d["sent"][0]["amount_nano"] == _AMOUNT_NANO @pytest.mark.asyncio async def test_limit_clamped_to_500(self) -> None: """Limit above 500 is clamped silently.""" from musehub.services.musehub_mcp_executor import execute_get_mpay_ledger from musehub.models.musehub import MPayLedgerResponse mock_ledger = MPayLedgerResponse( handle=_SENDER, sent=[], received=[], total_sent_nano=0, total_received_nano=0, ) captured: dict[str, int] = {} async def _mock_ledger(db: MagicMock, handle: str, limit: int = 100) -> MPayLedgerResponse: captured["limit"] = limit return mock_ledger with ( patch("musehub.services.musehub_mcp_executor._check_db_available", return_value=None), patch("musehub.services.musehub_mcp_executor.AsyncSessionLocal") as mock_session_cls, patch("musehub.services.musehub_mpay.get_mpay_ledger", new=_mock_ledger), ): mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=False) mock_session_cls.return_value = mock_session await execute_get_mpay_ledger(handle=_SENDER, limit=9999) assert captured["limit"] <= 500 # --------------------------------------------------------------------------- # 7. Dispatcher routing # --------------------------------------------------------------------------- class TestDispatcherRouting: """Dispatcher must route the 6 new tool names to the correct executors.""" @pytest.mark.asyncio async def test_dispatch_read_profile_manifest(self) -> None: from musehub.mcp.dispatcher import dispatch_tool mock_result = MagicMock(ok=True, data={"handle": "gabriel"}, error_code=None, error_message=None, hint=None) with patch( "musehub.services.musehub_mcp_executor.execute_read_profile_manifest", new=AsyncMock(return_value=mock_result), ) as mock_exe: await dispatch_tool( name="musehub_read_profile_manifest", arguments={"handle": "gabriel"}, user_id="gabriel", session_context=None, ) mock_exe.assert_called_once_with(handle="gabriel") @pytest.mark.asyncio async def test_dispatch_issue_attestation(self) -> None: from musehub.mcp.dispatcher import dispatch_tool mock_result = MagicMock(ok=True, data={}, error_code=None, error_message=None, hint=None) with patch( "musehub.services.musehub_mcp_executor.execute_issue_attestation", new=AsyncMock(return_value=mock_result), ) as mock_exe: args = { "attester": "gabriel", "subject": "aria", "claim": '{"type":"human"}', "issued_at_iso": _TS, "signature": "ed25519:abc", "attester_public_key": "ed25519:def", } await dispatch_tool( name="musehub_issue_attestation", arguments=args, user_id="gabriel", session_context=None, ) mock_exe.assert_called_once() @pytest.mark.asyncio async def test_dispatch_revoke_attestation(self) -> None: from musehub.mcp.dispatcher import dispatch_tool mock_result = MagicMock(ok=True, data={}, error_code=None, error_message=None, hint=None) with patch( "musehub.services.musehub_mcp_executor.execute_revoke_attestation", new=AsyncMock(return_value=mock_result), ) as mock_exe: await dispatch_tool( name="musehub_revoke_attestation", arguments={"attestation_id": long_id("a" * 64), "revoker": "gabriel"}, user_id="gabriel", session_context=None, ) mock_exe.assert_called_once() @pytest.mark.asyncio async def test_dispatch_list_attestations(self) -> None: from musehub.mcp.dispatcher import dispatch_tool mock_result = MagicMock(ok=True, data={}, error_code=None, error_message=None, hint=None) with patch( "musehub.services.musehub_mcp_executor.execute_list_attestations", new=AsyncMock(return_value=mock_result), ) as mock_exe: await dispatch_tool( name="musehub_list_attestations", arguments={"subject": "aria"}, user_id="gabriel", session_context=None, ) mock_exe.assert_called_once_with(subject="aria", include_revoked=False) @pytest.mark.asyncio async def test_dispatch_record_mpay_claim(self) -> None: from musehub.mcp.dispatcher import dispatch_tool mock_result = MagicMock(ok=True, data={}, error_code=None, error_message=None, hint=None) with patch( "musehub.services.musehub_mcp_executor.execute_record_mpay_claim", new=AsyncMock(return_value=mock_result), ) as mock_exe: args = { "sender": _SENDER, "recipient": _RECIPIENT, "amount_nano": _AMOUNT_NANO, "nonce_hex": _NONCE_HEX, "signature": "ed25519:abc", "sender_public_key": "ed25519:def", } await dispatch_tool( name="musehub_record_mpay_claim", arguments=args, user_id="gabriel", session_context=None, ) mock_exe.assert_called_once() @pytest.mark.asyncio async def test_dispatch_get_mpay_ledger(self) -> None: from musehub.mcp.dispatcher import dispatch_tool mock_result = MagicMock(ok=True, data={}, error_code=None, error_message=None, hint=None) with patch( "musehub.services.musehub_mcp_executor.execute_get_mpay_ledger", new=AsyncMock(return_value=mock_result), ) as mock_exe: await dispatch_tool( name="musehub_get_mpay_ledger", arguments={"handle": "gabriel", "limit": 50}, user_id="gabriel", session_context=None, ) mock_exe.assert_called_once_with(handle="gabriel", limit=50)