gabriel / musehub public

test_profile_reimagination.py file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:0 fix: fall back to any indexed mpack in read_object_bytes when push mpac… · gabriel · Jun 17, 2026
1 """Tests for Issue #1 — Profile Page Reimagination.
2
3 Covers:
4 1. Genesis ID functions for attestation and MPay
5 2. Canonical form / determinism / collision resistance
6 3. Attestation signature verification (pure, no DB)
7 4. MPay signature verification (pure, no DB)
8 5. Pydantic schema validation (ProfileManifest, AttestationResponse, MPayClaimResponse)
9 6. Service layer — attestation CRUD (async DB mocks)
10 7. Service layer — MPay claim record + ledger (async DB mocks)
11 8. Service layer — profile manifest builder (async DB mocks)
12 9. API route smoke tests (FastAPI TestClient)
13 """
14 from __future__ import annotations
15
16 import json
17 from datetime import datetime, timezone
18 from unittest.mock import AsyncMock, MagicMock, patch
19
20 import pytest
21 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
22 from muse.core.types import encode_pubkey, encode_sig, long_id, split_sig
23
24 from musehub.core.genesis import compute_attestation_id, compute_mpay_claim_id
25 from musehub.models.musehub import (
26 ActivityDomain,
27 AttestationBadge,
28 AttestationRequest,
29 AttestationResponse,
30 MPayClaimRequest,
31 MPayClaimResponse,
32 MPayLedgerResponse,
33 OrgManifest,
34 ProfileManifest,
35 ProfileRepoSummary,
36 TrustChainEntry,
37 )
38 from musehub.services.musehub_attestations import (
39 attestation_to_badge,
40 verify_attestation_signature,
41 )
42 from musehub.services.musehub_mpay import verify_mpay_signature
43
44 # ---------------------------------------------------------------------------
45 # Shared fixtures
46 # ---------------------------------------------------------------------------
47
48 _TS = "2026-04-21T12:00:00+00:00"
49 _ATTESTER = "gabriel"
50 _SUBJECT = "aria"
51 _CLAIM = '{"type":"human","confidence":0.99}'
52 _ATTESTER2 = "maestro"
53
54 _SENDER = "gabriel"
55 _RECIPIENT = "aria"
56 _AMOUNT_NANO = 1_000_000
57 _NONCE_HEX = "a" * 64
58
59
60 def _make_ed25519_pair() -> tuple[Ed25519PrivateKey, str]:
61 """Return (privkey, pubkey_prefixed)."""
62 privkey = Ed25519PrivateKey.generate()
63 pubkey_bytes = privkey.public_key().public_bytes_raw()
64 return privkey, encode_pubkey("ed25519", pubkey_bytes)
65
66
67 def _sign_attest(
68 privkey: Ed25519PrivateKey,
69 attester: str,
70 subject: str,
71 claim: str,
72 ts: str,
73 ) -> str:
74 msg = f"ATTEST\n{attester}\n{subject}\n{claim}\n{ts}".encode()
75 return encode_sig("ed25519", privkey.sign(msg))
76
77
78 def _sign_mpay(
79 privkey: Ed25519PrivateKey,
80 sender: str,
81 recipient: str,
82 amount_nano: int,
83 nonce_hex: str,
84 ) -> str:
85 msg = f"MPAY\n{sender}\n{recipient}\n{amount_nano}\n{nonce_hex}".encode()
86 return encode_sig("ed25519", privkey.sign(msg))
87
88
89 # ---------------------------------------------------------------------------
90 # 1 & 2. Genesis ID canonical form, determinism, collision resistance
91 # ---------------------------------------------------------------------------
92
93
94 class TestAttestationGenesisId:
95 def test_canonical_form(self) -> None:
96 aid = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, _TS)
97 assert aid.startswith("sha256:")
98 assert len(aid) == 71
99
100 def test_deterministic(self) -> None:
101 a1 = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, _TS)
102 a2 = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, _TS)
103 assert a1 == a2
104
105 def test_distinct_attester(self) -> None:
106 a1 = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, _TS)
107 a2 = compute_attestation_id(_ATTESTER2, _SUBJECT, _CLAIM, _TS)
108 assert a1 != a2
109
110 def test_distinct_subject(self) -> None:
111 a1 = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, _TS)
112 a2 = compute_attestation_id(_ATTESTER, "other_subject", _CLAIM, _TS)
113 assert a1 != a2
114
115 def test_distinct_claim(self) -> None:
116 a1 = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, _TS)
117 a2 = compute_attestation_id(_ATTESTER, _SUBJECT, '{"type":"org"}', _TS)
118 assert a1 != a2
119
120 def test_distinct_timestamp(self) -> None:
121 a1 = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, _TS)
122 a2 = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, "2026-05-01T00:00:00+00:00")
123 assert a1 != a2
124
125
126 class TestMPayGenesisId:
127 def test_canonical_form(self) -> None:
128 cid = compute_mpay_claim_id(_SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
129 assert cid.startswith("sha256:")
130 assert len(cid) == 71
131
132 def test_deterministic(self) -> None:
133 c1 = compute_mpay_claim_id(_SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
134 c2 = compute_mpay_claim_id(_SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
135 assert c1 == c2
136
137 def test_distinct_amount(self) -> None:
138 c1 = compute_mpay_claim_id(_SENDER, _RECIPIENT, 1_000_000, _NONCE_HEX)
139 c2 = compute_mpay_claim_id(_SENDER, _RECIPIENT, 2_000_000, _NONCE_HEX)
140 assert c1 != c2
141
142 def test_distinct_nonce(self) -> None:
143 c1 = compute_mpay_claim_id(_SENDER, _RECIPIENT, _AMOUNT_NANO, "a" * 64)
144 c2 = compute_mpay_claim_id(_SENDER, _RECIPIENT, _AMOUNT_NANO, "b" * 64)
145 assert c1 != c2
146
147
148 # ---------------------------------------------------------------------------
149 # 3. Attestation signature verification
150 # ---------------------------------------------------------------------------
151
152
153 class TestAttestationSignatureVerification:
154 def test_valid_signature(self) -> None:
155 privkey, pubkey = _make_ed25519_pair()
156 sig = _sign_attest(privkey, _ATTESTER, _SUBJECT, _CLAIM, _TS)
157 ok, reason = verify_attestation_signature(
158 _ATTESTER, _SUBJECT, _CLAIM, _TS, sig, pubkey
159 )
160 assert ok is True
161 assert reason == ""
162
163 def test_wrong_message_fails(self) -> None:
164 privkey, pubkey = _make_ed25519_pair()
165 # Sign with different claim
166 sig = _sign_attest(privkey, _ATTESTER, _SUBJECT, '{"type":"org"}', _TS)
167 ok, reason = verify_attestation_signature(
168 _ATTESTER, _SUBJECT, _CLAIM, _TS, sig, pubkey
169 )
170 assert ok is False
171
172 def test_missing_prefix_fails(self) -> None:
173 privkey, pubkey = _make_ed25519_pair()
174 sig = _sign_attest(privkey, _ATTESTER, _SUBJECT, _CLAIM, _TS)
175 bare_sig = split_sig(sig)[1]
176 ok, reason = verify_attestation_signature(
177 _ATTESTER, _SUBJECT, _CLAIM, _TS, bare_sig, pubkey
178 )
179 assert ok is False
180 assert "ed25519:" in reason
181
182 def test_wrong_public_key_fails(self) -> None:
183 privkey, _ = _make_ed25519_pair()
184 _, other_pubkey = _make_ed25519_pair()
185 sig = _sign_attest(privkey, _ATTESTER, _SUBJECT, _CLAIM, _TS)
186 ok, _ = verify_attestation_signature(
187 _ATTESTER, _SUBJECT, _CLAIM, _TS, sig, other_pubkey
188 )
189 assert ok is False
190
191
192 # ---------------------------------------------------------------------------
193 # 4. MPay signature verification
194 # ---------------------------------------------------------------------------
195
196
197 class TestMPaySignatureVerification:
198 def test_valid_signature(self) -> None:
199 privkey, pubkey = _make_ed25519_pair()
200 sig = _sign_mpay(privkey, _SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
201 ok, reason = verify_mpay_signature(
202 _SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX, sig, pubkey
203 )
204 assert ok is True
205 assert reason == ""
206
207 def test_tampered_amount_fails(self) -> None:
208 privkey, pubkey = _make_ed25519_pair()
209 sig = _sign_mpay(privkey, _SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
210 # Verify with different amount
211 ok, _ = verify_mpay_signature(
212 _SENDER, _RECIPIENT, _AMOUNT_NANO + 1, _NONCE_HEX, sig, pubkey
213 )
214 assert ok is False
215
216 def test_missing_prefix_fails(self) -> None:
217 privkey, pubkey = _make_ed25519_pair()
218 sig = _sign_mpay(privkey, _SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
219 bare_sig = split_sig(sig)[1]
220 ok, reason = verify_mpay_signature(
221 _SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX, bare_sig, pubkey
222 )
223 assert ok is False
224
225
226 # ---------------------------------------------------------------------------
227 # 5. Pydantic schema validation
228 # ---------------------------------------------------------------------------
229
230
231 class TestPydanticSchemas:
232 def _make_repo_summary(self) -> ProfileRepoSummary:
233 return ProfileRepoSummary(
234 repo_id=long_id("a" * 64),
235 name="test-repo",
236 owner="gabriel",
237 slug="test-repo",
238 visibility="public",
239 last_activity_at=None,
240 created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
241 )
242
243 def test_profile_manifest_human(self) -> None:
244 manifest = ProfileManifest(
245 identity_id=long_id("a" * 64),
246 handle="gabriel",
247 identity_type="human",
248 avax_address="0xABCDEF",
249 created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
250 updated_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
251 )
252 assert manifest.identity_type == "human"
253 assert manifest.avax_address == "0xABCDEF"
254 assert manifest.org is None
255 assert manifest.trust_chain == []
256
257 def test_profile_manifest_agent(self) -> None:
258 manifest = ProfileManifest(
259 identity_id=long_id("b" * 64),
260 handle="aria",
261 identity_type="agent",
262 agent_model="claude-sonnet-4-6",
263 agent_capabilities=["push", "pull"],
264 trust_chain=[
265 TrustChainEntry(handle="aria", identity_type="agent", spawned_by="gabriel"),
266 TrustChainEntry(handle="gabriel", identity_type="human", spawned_by=None),
267 ],
268 created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
269 updated_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
270 )
271 assert manifest.identity_type == "agent"
272 assert len(manifest.trust_chain) == 2
273 assert manifest.trust_chain[-1].identity_type == "human"
274
275 def test_profile_manifest_org(self) -> None:
276 manifest = ProfileManifest(
277 identity_id=long_id("c" * 64),
278 handle="acme",
279 identity_type="org",
280 org=OrgManifest(members=["gabriel", "aria"], quorum=2),
281 created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
282 updated_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
283 )
284 assert manifest.org is not None
285 assert manifest.org.quorum == 2
286
287 def test_activity_domain_grid_length(self) -> None:
288 grid = [0] * 364
289 domain = ActivityDomain(domain="code", grid=grid, peak=0, total=0)
290 assert len(domain.grid) == 364
291
292 def test_attestation_response(self) -> None:
293 issued = datetime(2026, 4, 21, 12, 0, 0, tzinfo=timezone.utc)
294 resp = AttestationResponse(
295 attestation_id=long_id("d" * 64),
296 attester="gabriel",
297 subject="aria",
298 claim='{"type":"human"}',
299 signature="ed25519:fakesig",
300 attester_public_key="ed25519:fakepubkey",
301 issued_at=issued,
302 )
303 assert resp.revoked_at is None
304
305 def test_attestation_badge_claim_type(self) -> None:
306 issued = datetime(2026, 4, 21, 12, 0, 0, tzinfo=timezone.utc)
307 a = AttestationResponse(
308 attestation_id=long_id("e" * 64),
309 attester="gabriel",
310 subject="aria",
311 claim='{"type":"human","confidence":0.99}',
312 signature="ed25519:sig",
313 attester_public_key="ed25519:pk",
314 issued_at=issued,
315 )
316 badge = attestation_to_badge(a)
317 assert badge.claim_type == "human"
318
319 def test_mpay_claim_response(self) -> None:
320 now = datetime(2026, 4, 21, tzinfo=timezone.utc)
321 resp = MPayClaimResponse(
322 claim_id=long_id("f" * 64),
323 sender="gabriel",
324 recipient="aria",
325 amount_nano=1_000_000,
326 nonce_hex="a" * 64,
327 signature="ed25519:sig",
328 sender_public_key="ed25519:pk",
329 created_at=now,
330 )
331 assert resp.confirmed_at is None
332 assert resp.voided_at is None
333
334 def test_mpay_ledger_totals(self) -> None:
335 now = datetime(2026, 4, 21, tzinfo=timezone.utc)
336
337 def _claim(amount: int) -> MPayClaimResponse:
338 return MPayClaimResponse(
339 claim_id=long_id("a" * 64),
340 sender="gabriel",
341 recipient="aria",
342 amount_nano=amount,
343 nonce_hex="b" * 64,
344 signature="ed25519:sig",
345 sender_public_key="ed25519:pk",
346 created_at=now,
347 )
348
349 ledger = MPayLedgerResponse(
350 handle="gabriel",
351 sent=[_claim(500_000), _claim(250_000)],
352 received=[_claim(1_000_000)],
353 total_sent_nano=750_000,
354 total_received_nano=1_000_000,
355 )
356 assert ledger.total_sent_nano == 750_000
357 assert ledger.total_received_nano == 1_000_000
358
359
360 # ---------------------------------------------------------------------------
361 # 6. Service layer — attestation (async DB mocks)
362 # ---------------------------------------------------------------------------
363
364
365 class TestAttestationService:
366 @pytest.mark.asyncio
367 async def test_issue_attestation_valid_sig(self) -> None:
368 privkey, pubkey = _make_ed25519_pair()
369 issued_at = datetime(2026, 4, 21, 12, 0, 0, tzinfo=timezone.utc)
370 sig = _sign_attest(privkey, _ATTESTER, _SUBJECT, _CLAIM, issued_at.isoformat())
371
372 req = AttestationRequest(
373 attester=_ATTESTER,
374 subject=_SUBJECT,
375 claim=_CLAIM,
376 signature=sig,
377 attester_public_key=pubkey,
378 issued_at=issued_at,
379 )
380
381 # Mock DB: no existing record → insert
382 mock_db = AsyncMock()
383 mock_result = MagicMock()
384 mock_result.mappings.return_value.one_or_none.return_value = None
385 mock_db.execute.return_value = mock_result
386
387 from musehub.services.musehub_attestations import issue_attestation
388 result = await issue_attestation(mock_db, req)
389
390 assert result.attester == _ATTESTER
391 assert result.subject == _SUBJECT
392 assert result.revoked_at is None
393 mock_db.commit.assert_called_once()
394
395 @pytest.mark.asyncio
396 async def test_issue_attestation_invalid_sig_raises(self) -> None:
397 _, pubkey = _make_ed25519_pair()
398 issued_at = datetime(2026, 4, 21, 12, 0, 0, tzinfo=timezone.utc)
399
400 req = AttestationRequest(
401 attester=_ATTESTER,
402 subject=_SUBJECT,
403 claim=_CLAIM,
404 signature="ed25519:invalidsignaturedata",
405 attester_public_key=pubkey,
406 issued_at=issued_at,
407 )
408 mock_db = AsyncMock()
409 mock_result = MagicMock()
410 mock_result.mappings.return_value.one_or_none.return_value = None
411 mock_db.execute.return_value = mock_result
412
413 from musehub.services.musehub_attestations import issue_attestation
414 with pytest.raises(ValueError, match="Invalid attestation signature"):
415 await issue_attestation(mock_db, req)
416
417 @pytest.mark.asyncio
418 async def test_issue_attestation_idempotent(self) -> None:
419 privkey, pubkey = _make_ed25519_pair()
420 issued_at = datetime(2026, 4, 21, 12, 0, 0, tzinfo=timezone.utc)
421 sig = _sign_attest(privkey, _ATTESTER, _SUBJECT, _CLAIM, issued_at.isoformat())
422 aid = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, issued_at.isoformat())
423
424 req = AttestationRequest(
425 attester=_ATTESTER,
426 subject=_SUBJECT,
427 claim=_CLAIM,
428 signature=sig,
429 attester_public_key=pubkey,
430 issued_at=issued_at,
431 )
432
433 existing_row = {
434 "attestation_id": aid,
435 "attester": _ATTESTER,
436 "subject": _SUBJECT,
437 "claim": _CLAIM,
438 "signature": sig,
439 "attester_public_key": pubkey,
440 "issued_at": issued_at,
441 "revoked_at": None,
442 }
443 mock_db = AsyncMock()
444 mock_result = MagicMock()
445 mock_result.mappings.return_value.one_or_none.return_value = existing_row
446 mock_db.execute.return_value = mock_result
447
448 from musehub.services.musehub_attestations import issue_attestation
449 result = await issue_attestation(mock_db, req)
450
451 # Should return existing without insert
452 assert result.attestation_id == aid
453 mock_db.commit.assert_not_called()
454
455 @pytest.mark.asyncio
456 async def test_revoke_attestation_wrong_revoker_raises(self) -> None:
457 aid = compute_attestation_id(_ATTESTER, _SUBJECT, _CLAIM, _TS)
458 existing_row = {
459 "attestation_id": aid,
460 "attester": _ATTESTER,
461 "subject": _SUBJECT,
462 "claim": _CLAIM,
463 "signature": "ed25519:sig",
464 "attester_public_key": "ed25519:pk",
465 "issued_at": datetime(2026, 4, 21, tzinfo=timezone.utc),
466 "revoked_at": None,
467 }
468 mock_db = AsyncMock()
469 mock_result = MagicMock()
470 mock_result.mappings.return_value.one_or_none.return_value = existing_row
471 mock_db.execute.return_value = mock_result
472
473 from musehub.services.musehub_attestations import revoke_attestation
474 with pytest.raises(PermissionError):
475 await revoke_attestation(mock_db, aid, revoker="not_gabriel")
476
477 @pytest.mark.asyncio
478 async def test_revoke_attestation_not_found_raises(self) -> None:
479 mock_db = AsyncMock()
480 mock_result = MagicMock()
481 mock_result.mappings.return_value.one_or_none.return_value = None
482 mock_db.execute.return_value = mock_result
483
484 from musehub.services.musehub_attestations import revoke_attestation
485 with pytest.raises(KeyError):
486 await revoke_attestation(mock_db, long_id("x" * 64), revoker=_ATTESTER)
487
488
489 # ---------------------------------------------------------------------------
490 # 7. Service layer — MPay claim (async DB mocks)
491 # ---------------------------------------------------------------------------
492
493
494 class TestMPayService:
495 @pytest.mark.asyncio
496 async def test_record_claim_valid_sig(self) -> None:
497 privkey, pubkey = _make_ed25519_pair()
498 sig = _sign_mpay(privkey, _SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
499
500 req = MPayClaimRequest(
501 sender=_SENDER,
502 recipient=_RECIPIENT,
503 amount_nano=_AMOUNT_NANO,
504 nonce_hex=_NONCE_HEX,
505 signature=sig,
506 sender_public_key=pubkey,
507 )
508
509 mock_db = AsyncMock()
510 mock_result = MagicMock()
511 mock_result.mappings.return_value.one_or_none.return_value = None
512 mock_db.execute.return_value = mock_result
513
514 from musehub.services.musehub_mpay import record_mpay_claim
515 result = await record_mpay_claim(mock_db, req)
516
517 assert result.sender == _SENDER
518 assert result.recipient == _RECIPIENT
519 assert result.amount_nano == _AMOUNT_NANO
520 mock_db.commit.assert_called_once()
521
522 @pytest.mark.asyncio
523 async def test_record_claim_invalid_sig_raises(self) -> None:
524 _, pubkey = _make_ed25519_pair()
525
526 req = MPayClaimRequest(
527 sender=_SENDER,
528 recipient=_RECIPIENT,
529 amount_nano=_AMOUNT_NANO,
530 nonce_hex=_NONCE_HEX,
531 signature="ed25519:badsig",
532 sender_public_key=pubkey,
533 )
534 mock_db = AsyncMock()
535 mock_result = MagicMock()
536 mock_result.mappings.return_value.one_or_none.return_value = None
537 mock_db.execute.return_value = mock_result
538
539 from musehub.services.musehub_mpay import record_mpay_claim
540 with pytest.raises(ValueError, match="Invalid MPay signature"):
541 await record_mpay_claim(mock_db, req)
542
543 @pytest.mark.asyncio
544 async def test_record_claim_idempotent(self) -> None:
545 privkey, pubkey = _make_ed25519_pair()
546 sig = _sign_mpay(privkey, _SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
547 claim_id = compute_mpay_claim_id(_SENDER, _RECIPIENT, _AMOUNT_NANO, _NONCE_HEX)
548 now = datetime(2026, 4, 21, tzinfo=timezone.utc)
549
550 existing_row = {
551 "claim_id": claim_id,
552 "sender": _SENDER,
553 "recipient": _RECIPIENT,
554 "amount_nano": _AMOUNT_NANO,
555 "nonce_hex": _NONCE_HEX,
556 "signature": sig,
557 "sender_public_key": pubkey,
558 "memo": None,
559 "created_at": now,
560 "confirmed_at": None,
561 "voided_at": None,
562 }
563 mock_db = AsyncMock()
564 mock_result = MagicMock()
565 mock_result.mappings.return_value.one_or_none.return_value = existing_row
566 mock_db.execute.return_value = mock_result
567
568 req = MPayClaimRequest(
569 sender=_SENDER,
570 recipient=_RECIPIENT,
571 amount_nano=_AMOUNT_NANO,
572 nonce_hex=_NONCE_HEX,
573 signature=sig,
574 sender_public_key=pubkey,
575 )
576
577 from musehub.services.musehub_mpay import record_mpay_claim
578 result = await record_mpay_claim(mock_db, req)
579
580 assert result.claim_id == claim_id
581 mock_db.commit.assert_not_called()
582
583
584 # ---------------------------------------------------------------------------
585 # 8. Service layer — profile manifest builder
586 # ---------------------------------------------------------------------------
587
588
589 class TestProfileManifestBuilder:
590 def _make_identity(
591 self,
592 handle: str = "gabriel",
593 identity_type: str = "human",
594 spawned_by: str | None = None,
595 ) -> MagicMock:
596 identity = MagicMock()
597 identity.identity_id = long_id("a" * 64)
598 identity.handle = handle
599 identity.identity_type = identity_type
600 identity.display_name = f"Display {handle}"
601 identity.bio = None
602 identity.avatar_url = None
603 identity.location = None
604 identity.website_url = None
605 identity.social_url = None
606 identity.is_verified = False
607 identity.cc_license = None
608 identity.pinned_repo_ids = []
609 identity.avax_address = "0xABC" if identity_type == "human" else None
610 identity.agent_model = "claude-sonnet-4-6" if identity_type == "agent" else None
611 identity.agent_capabilities = ["push"] if identity_type == "agent" else []
612 identity.org_members = ["gabriel", "aria"] if identity_type == "org" else None
613 identity.org_quorum = 2 if identity_type == "org" else None
614 identity.org_treasury_address = None
615 identity.spawned_by = spawned_by
616 identity.deleted_at = None
617 identity.created_at = datetime(2026, 1, 1, tzinfo=timezone.utc)
618 identity.updated_at = datetime(2026, 1, 1, tzinfo=timezone.utc)
619 return identity
620
621 @pytest.mark.asyncio
622 async def test_manifest_returns_none_for_unknown_handle(self) -> None:
623 from musehub.services.musehub_profile import build_profile_manifest
624
625 with patch(
626 "musehub.services.musehub_profile.get_profile_by_username",
627 new=AsyncMock(return_value=None),
628 ):
629 result = await build_profile_manifest(AsyncMock(), "unknown")
630 assert result is None
631
632 @pytest.mark.asyncio
633 async def test_manifest_human_fields(self) -> None:
634 from musehub.services.musehub_profile import build_profile_manifest
635
636 identity = self._make_identity("gabriel", "human")
637
638 with (
639 patch("musehub.services.musehub_profile.get_profile_by_username", new=AsyncMock(return_value=identity)),
640 patch("musehub.services.musehub_profile.get_public_repos", new=AsyncMock(return_value=[])),
641 patch("musehub.services.musehub_profile.build_activity_canvas", new=AsyncMock(return_value=[])),
642 ):
643 result = await build_profile_manifest(AsyncMock(), "gabriel")
644
645 assert result is not None
646 assert result.identity_type == "human"
647 assert result.avax_address == "0xABC"
648 assert result.org is None
649 assert result.trust_chain == []
650
651 @pytest.mark.asyncio
652 async def test_manifest_org_fields(self) -> None:
653 from musehub.services.musehub_profile import build_profile_manifest
654
655 identity = self._make_identity("acme", "org")
656
657 with (
658 patch("musehub.services.musehub_profile.get_profile_by_username", new=AsyncMock(return_value=identity)),
659 patch("musehub.services.musehub_profile.get_public_repos", new=AsyncMock(return_value=[])),
660 patch("musehub.services.musehub_profile.build_activity_canvas", new=AsyncMock(return_value=[])),
661 ):
662 result = await build_profile_manifest(AsyncMock(), "acme")
663
664 assert result is not None
665 assert result.org is not None
666 assert result.org.members == ["gabriel", "aria"]
667 assert result.org.quorum == 2
668 assert result.avax_address is None
669
670 @pytest.mark.asyncio
671 async def test_manifest_mpay_totals_passed_through(self) -> None:
672 from musehub.services.musehub_profile import build_profile_manifest
673
674 identity = self._make_identity("gabriel", "human")
675
676 with (
677 patch("musehub.services.musehub_profile.get_profile_by_username", new=AsyncMock(return_value=identity)),
678 patch("musehub.services.musehub_profile.get_public_repos", new=AsyncMock(return_value=[])),
679 patch("musehub.services.musehub_profile.build_activity_canvas", new=AsyncMock(return_value=[])),
680 ):
681 result = await build_profile_manifest(
682 AsyncMock(), "gabriel", mpay_sent_nano=500_000, mpay_received_nano=1_000_000
683 )
684
685 assert result is not None
686 assert result.mpay_total_sent_nano == 500_000
687 assert result.mpay_total_received_nano == 1_000_000
688
689
690 # ---------------------------------------------------------------------------
691 # 9. Activity canvas unit tests
692 # ---------------------------------------------------------------------------
693
694
695 class TestActivityCanvas:
696 def test_grid_index_today_is_last(self) -> None:
697 from musehub.services.musehub_profile import _date_to_grid_index, _GRID_DAYS
698
699 today = datetime(2026, 4, 21, tzinfo=timezone.utc)
700 idx = _date_to_grid_index(today, today)
701 assert idx == _GRID_DAYS - 1
702
703 def test_grid_index_out_of_range_returns_none(self) -> None:
704 from musehub.services.musehub_profile import _date_to_grid_index
705
706 today = datetime(2026, 4, 21, tzinfo=timezone.utc)
707 past = datetime(2025, 1, 1, tzinfo=timezone.utc) # >364 days ago
708 idx = _date_to_grid_index(today, past)
709 assert idx is None
710
711 def test_grid_index_future_returns_none(self) -> None:
712 from musehub.services.musehub_profile import _date_to_grid_index
713
714 today = datetime(2026, 4, 21, tzinfo=timezone.utc)
715 future = datetime(2026, 5, 1, tzinfo=timezone.utc)
716 idx = _date_to_grid_index(today, future)
717 assert idx is None
718
719 def test_empty_grid_length(self) -> None:
720 from musehub.services.musehub_profile import _empty_grid, _GRID_DAYS
721
722 assert len(_empty_grid()) == _GRID_DAYS
723
724 def test_grid_to_domain(self) -> None:
725 from musehub.services.musehub_profile import _grid_to_domain
726
727 grid = [0] * 363 + [5]
728 domain = _grid_to_domain("code", grid)
729 assert domain.peak == 5
730 assert domain.total == 5
731 assert domain.domain == "code"