gabriel / musehub public

test_spectral_sigil.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 """TDD tests for Spectral Sigil — content-addressed SVG avatar generation.
2
3 Route: GET /avatars/{identity_id}.svg
4 Generator: musehub.services.spectral_sigil
5
6 Tests are RED-first. Each assertion drives one concrete implementation decision.
7 """
8 from __future__ import annotations
9
10 import hashlib
11 import re
12 import xml.etree.ElementTree as ET
13 from unittest.mock import AsyncMock, MagicMock, patch
14
15 import pytest
16 from httpx import ASGITransport, AsyncClient
17
18 from musehub.main import app
19
20 # ---------------------------------------------------------------------------
21 # Fixtures and helpers
22 # ---------------------------------------------------------------------------
23
24 # A valid sha256-style identity_id (no "sha256:" prefix — bare hex)
25 _HUMAN_ID = "a" * 64
26 _AGENT_ID = "b" * 64
27 _ORG_ID = "c" * 64
28
29 # identity_id whose bytes[0:4] set hue to a known value for assertions
30 _KNOWN_HUE_ID = "00000000" + "a" * 56 # bytes 0-3 = 0x00000000 → hue 0 (red)
31
32
33 def _parse_svg(content: bytes) -> ET.Element:
34 return ET.fromstring(content)
35
36
37 def _make_identity_mock(
38 identity_id: str,
39 handle: str,
40 identity_type: str,
41 domain_counts: dict[str, int] | None = None,
42 ) -> MagicMock:
43 mock = MagicMock()
44 mock.identity_id = identity_id
45 mock.handle = handle
46 mock.identity_type = identity_type
47 mock.domain_counts = domain_counts or {}
48 return mock
49
50
51 # ---------------------------------------------------------------------------
52 # Module import — must exist
53 # ---------------------------------------------------------------------------
54
55
56 def test_spectral_sigil_module_importable() -> None:
57 from musehub.services import spectral_sigil # noqa: F401
58
59
60 def test_spectral_sigil_generator_callable() -> None:
61 from musehub.services.spectral_sigil import generate_sigil
62
63 assert callable(generate_sigil)
64
65
66 # ---------------------------------------------------------------------------
67 # Core generation contract
68 # ---------------------------------------------------------------------------
69
70
71 def test_generate_sigil_returns_bytes() -> None:
72 from musehub.services.spectral_sigil import generate_sigil
73
74 result = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
75 assert isinstance(result, bytes)
76
77
78 def test_generate_sigil_is_valid_svg() -> None:
79 from musehub.services.spectral_sigil import generate_sigil
80
81 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
82 root = _parse_svg(svg_bytes)
83 # Root element must be <svg>
84 assert root.tag in {"svg", "{http://www.w3.org/2000/svg}svg"}
85
86
87 def test_generate_sigil_has_viewbox() -> None:
88 from musehub.services.spectral_sigil import generate_sigil
89
90 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
91 root = _parse_svg(svg_bytes)
92 assert "viewBox" in root.attrib
93
94
95 def test_generate_sigil_deterministic_same_id() -> None:
96 from musehub.services.spectral_sigil import generate_sigil
97
98 a = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
99 b = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
100 assert a == b
101
102
103 def test_generate_sigil_deterministic_same_domains() -> None:
104 from musehub.services.spectral_sigil import generate_sigil
105
106 domains = {"code": 10, "music": 5}
107 a = generate_sigil(_HUMAN_ID, "gabriel", "human", domains)
108 b = generate_sigil(_HUMAN_ID, "gabriel", "human", domains)
109 assert a == b
110
111
112 def test_generate_sigil_differs_by_identity_id() -> None:
113 from musehub.services.spectral_sigil import generate_sigil
114
115 a = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
116 b = generate_sigil(_AGENT_ID, "gabriel", "human", {})
117 assert a != b
118
119
120 # ---------------------------------------------------------------------------
121 # Archetype — distinct SVG structure
122 # ---------------------------------------------------------------------------
123
124
125 def test_human_sigil_contains_path_element() -> None:
126 """Human archetype uses organic bezier blob → at least one <path>."""
127 from musehub.services.spectral_sigil import generate_sigil
128
129 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
130 root = _parse_svg(svg_bytes)
131 # ET uses Clark notation {ns}tag for namespaced elements
132 paths = (
133 root.findall(".//{http://www.w3.org/2000/svg}path")
134 or root.findall(".//path")
135 )
136 assert len(paths) >= 1, "human sigil must contain at least one <path> (bezier blob)"
137
138
139 def test_agent_sigil_contains_polygon_or_path() -> None:
140 """Agent archetype uses hexagonal circuit trace → polygon or path."""
141 from musehub.services.spectral_sigil import generate_sigil
142
143 svg_bytes = generate_sigil(_AGENT_ID, "aria", "agent", {})
144 root = _parse_svg(svg_bytes)
145 polygons = root.findall(".//polygon") or root.findall(
146 ".//{http://www.w3.org/2000/svg}polygon"
147 )
148 paths = root.findall(".//path") or root.findall(".//{http://www.w3.org/2000/svg}path")
149 assert len(polygons) + len(paths) >= 1, "agent sigil must contain polygon or path (hex)"
150
151
152 def test_org_sigil_contains_polygon_or_path() -> None:
153 """Org archetype uses quorum polygon → polygon or path."""
154 from musehub.services.spectral_sigil import generate_sigil
155
156 svg_bytes = generate_sigil(_ORG_ID, "tellurstori", "org", {})
157 root = _parse_svg(svg_bytes)
158 polygons = root.findall(".//polygon") or root.findall(
159 ".//{http://www.w3.org/2000/svg}polygon"
160 )
161 paths = root.findall(".//path") or root.findall(".//{http://www.w3.org/2000/svg}path")
162 assert len(polygons) + len(paths) >= 1, "org sigil must contain polygon or path"
163
164
165 def test_archetypes_produce_distinct_svgs() -> None:
166 """Same identity_id bytes but different archetype → different output."""
167 from musehub.services.spectral_sigil import generate_sigil
168
169 human_svg = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
170 agent_svg = generate_sigil(_HUMAN_ID, "gabriel", "agent", {})
171 org_svg = generate_sigil(_HUMAN_ID, "gabriel", "org", {})
172 # All three must differ
173 assert human_svg != agent_svg
174 assert human_svg != org_svg
175 assert agent_svg != org_svg
176
177
178 # ---------------------------------------------------------------------------
179 # Hue derivation from identity_id bytes
180 # ---------------------------------------------------------------------------
181
182
183 def test_hue_derives_from_identity_id_bytes() -> None:
184 """bytes[0:4] of identity_id → hue in 0-360. Verify formula matches implementation."""
185 from musehub.services.spectral_sigil import derive_hue
186
187 # 0x00000000 → hue 0
188 assert derive_hue("00000000" + "a" * 56) == 0
189 # 0xffffffff = 4294967295; 4294967295 % 360 = 255
190 assert derive_hue("ffffffff" + "a" * 56) == 4294967295 % 360
191
192
193 def test_hue_function_importable() -> None:
194 from musehub.services.spectral_sigil import derive_hue # noqa: F401
195
196 assert callable(derive_hue)
197
198
199 # ---------------------------------------------------------------------------
200 # Orbital rings — derived from bytes 8-15
201 # ---------------------------------------------------------------------------
202
203
204 def test_sigil_contains_ellipse_elements() -> None:
205 """Orbital rings are rendered as <ellipse> or <circle> elements."""
206 from musehub.services.spectral_sigil import generate_sigil
207
208 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
209 root = _parse_svg(svg_bytes)
210 ellipses = root.findall(".//ellipse") or root.findall(
211 ".//{http://www.w3.org/2000/svg}ellipse"
212 )
213 circles = root.findall(".//circle") or root.findall(
214 ".//{http://www.w3.org/2000/svg}circle"
215 )
216 assert len(ellipses) + len(circles) >= 2, "at least 2 orbital rings expected"
217
218
219 # ---------------------------------------------------------------------------
220 # Domain dots — colored by domain activity
221 # ---------------------------------------------------------------------------
222
223
224 def test_domain_dots_absent_when_no_activity() -> None:
225 """With no domain counts, domain dots should be grayscale / muted."""
226 from musehub.services.spectral_sigil import generate_sigil
227
228 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
229 svg_text = svg_bytes.decode()
230 # Domain-specific colors must NOT appear when there's no activity
231 domain_colors = ["#388bfd", "#bc8cff", "#3fb950", "#f0883e", "#d29922"]
232 for color in domain_colors:
233 assert color.lower() not in svg_text.lower(), (
234 f"domain color {color} should not appear with no activity"
235 )
236
237
238 def test_domain_dot_code_color_present_when_active() -> None:
239 """With code activity, the code domain color #388bfd must appear in SVG."""
240 from musehub.services.spectral_sigil import generate_sigil
241
242 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {"code": 42})
243 svg_text = svg_bytes.decode()
244 assert "#388bfd" in svg_text.lower() or "388bfd" in svg_text.lower(), (
245 "code domain color #388bfd expected when code activity > 0"
246 )
247
248
249 def test_domain_dot_music_color_present_when_active() -> None:
250 from musehub.services.spectral_sigil import generate_sigil
251
252 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {"music": 7})
253 svg_text = svg_bytes.decode()
254 assert "#bc8cff" in svg_text.lower() or "bc8cff" in svg_text.lower()
255
256
257 def test_domain_dot_midi_color_present_when_active() -> None:
258 from musehub.services.spectral_sigil import generate_sigil
259
260 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {"midi": 3})
261 svg_text = svg_bytes.decode()
262 assert "#3fb950" in svg_text.lower() or "3fb950" in svg_text.lower()
263
264
265 def test_domain_dot_mpay_color_present_when_active() -> None:
266 from musehub.services.spectral_sigil import generate_sigil
267
268 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {"mpay": 1})
269 svg_text = svg_bytes.decode()
270 assert "#d29922" in svg_text.lower() or "d29922" in svg_text.lower()
271
272
273 # ---------------------------------------------------------------------------
274 # Handle initial — center text
275 # ---------------------------------------------------------------------------
276
277
278 def test_sigil_contains_text_with_first_char() -> None:
279 """The SVG must contain a <text> element with the first char of the handle."""
280 from musehub.services.spectral_sigil import generate_sigil
281
282 svg_bytes = generate_sigil(_HUMAN_ID, "gabriel", "human", {})
283 root = _parse_svg(svg_bytes)
284 texts = root.findall(".//text") or root.findall(
285 ".//{http://www.w3.org/2000/svg}text"
286 )
287 assert len(texts) >= 1, "sigil must contain at least one <text> element"
288 found = any("g" in (t.text or "").lower() for t in texts)
289 assert found, "first char 'g' of handle 'gabriel' must appear in a <text> element"
290
291
292 def test_sigil_text_initial_matches_handle() -> None:
293 from musehub.services.spectral_sigil import generate_sigil
294
295 svg_bytes = generate_sigil(_AGENT_ID, "aria", "agent", {})
296 root = _parse_svg(svg_bytes)
297 texts = root.findall(".//text") or root.findall(
298 ".//{http://www.w3.org/2000/svg}text"
299 )
300 found = any("a" in (t.text or "").lower() for t in texts)
301 assert found, "first char 'a' of handle 'aria' must appear in a <text> element"
302
303
304 # ---------------------------------------------------------------------------
305 # HTTP route — GET /avatars/{algo}/{hex}.svg
306 # ---------------------------------------------------------------------------
307
308 _SHA256_URL = f"/avatars/sha256/{_HUMAN_ID}.svg"
309
310
311 @pytest.mark.asyncio
312 async def test_avatar_route_returns_200_for_valid_id(db_session: AsyncMock) -> None:
313 """GET /avatars/sha256/{hex}.svg → 200 image/svg+xml."""
314 mock_identity = _make_identity_mock(_HUMAN_ID, "gabriel", "human", {})
315
316 with patch(
317 "musehub.api.routes.musehub.ui_avatars._fetch_identity",
318 new=AsyncMock(return_value=mock_identity),
319 ):
320 async with AsyncClient(
321 transport=ASGITransport(app=app), base_url="http://test"
322 ) as client:
323 response = await client.get(_SHA256_URL)
324
325 assert response.status_code == 200
326 assert "image/svg+xml" in response.headers["content-type"]
327
328
329 @pytest.mark.asyncio
330 async def test_avatar_route_svg_body_is_valid(db_session: AsyncMock) -> None:
331 mock_identity = _make_identity_mock(_HUMAN_ID, "gabriel", "human", {})
332
333 with patch(
334 "musehub.api.routes.musehub.ui_avatars._fetch_identity",
335 new=AsyncMock(return_value=mock_identity),
336 ):
337 async with AsyncClient(
338 transport=ASGITransport(app=app), base_url="http://test"
339 ) as client:
340 response = await client.get(_SHA256_URL)
341
342 root = _parse_svg(response.content)
343 assert root.tag in {"svg", "{http://www.w3.org/2000/svg}svg"}
344
345
346 @pytest.mark.asyncio
347 async def test_avatar_route_has_immutable_cache_headers(db_session: AsyncMock) -> None:
348 """Content-addressed: same id → same bytes forever → immutable cache."""
349 mock_identity = _make_identity_mock(_HUMAN_ID, "gabriel", "human", {})
350
351 with patch(
352 "musehub.api.routes.musehub.ui_avatars._fetch_identity",
353 new=AsyncMock(return_value=mock_identity),
354 ):
355 async with AsyncClient(
356 transport=ASGITransport(app=app), base_url="http://test"
357 ) as client:
358 response = await client.get(_SHA256_URL)
359
360 cache = response.headers.get("cache-control", "")
361 assert "immutable" in cache or "max-age=31536000" in cache
362
363
364 @pytest.mark.asyncio
365 async def test_avatar_route_returns_404_for_unknown_id(db_session: AsyncMock) -> None:
366 """Unknown identity_id → 404."""
367 with patch(
368 "musehub.api.routes.musehub.ui_avatars._fetch_identity",
369 new=AsyncMock(return_value=None),
370 ):
371 async with AsyncClient(
372 transport=ASGITransport(app=app), base_url="http://test"
373 ) as client:
374 response = await client.get(_SHA256_URL)
375
376 assert response.status_code == 404
377
378
379 @pytest.mark.asyncio
380 async def test_avatar_route_returns_400_for_invalid_hex(db_session: AsyncMock) -> None:
381 """Non-hex or wrong-length digest → 400."""
382 async with AsyncClient(
383 transport=ASGITransport(app=app), base_url="http://test"
384 ) as client:
385 response = await client.get("/avatars/sha256/not-a-valid-hex.svg")
386
387 assert response.status_code == 400
388
389
390 @pytest.mark.asyncio
391 async def test_avatar_route_returns_400_for_unknown_algo(db_session: AsyncMock) -> None:
392 """Unknown algo → 400."""
393 async with AsyncClient(
394 transport=ASGITransport(app=app), base_url="http://test"
395 ) as client:
396 response = await client.get(f"/avatars/md5/{_HUMAN_ID}.svg")
397
398 assert response.status_code == 400
399
400
401 @pytest.mark.asyncio
402 async def test_avatar_route_deterministic_across_requests(db_session: AsyncMock) -> None:
403 """Two identical requests must return identical bodies."""
404 mock_identity = _make_identity_mock(_HUMAN_ID, "gabriel", "human", {"code": 5})
405
406 with patch(
407 "musehub.api.routes.musehub.ui_avatars._fetch_identity",
408 new=AsyncMock(return_value=mock_identity),
409 ):
410 async with AsyncClient(
411 transport=ASGITransport(app=app), base_url="http://test"
412 ) as client:
413 r1 = await client.get(_SHA256_URL)
414 r2 = await client.get(_SHA256_URL)
415
416 assert r1.content == r2.content