gabriel / musehub public

test_musehub_profile_snapshot.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 the profile snapshot pre-computation pipeline.
2
3 Covers:
4 - test_snapshot_table_is_queryable β€” MusehubProfileSnapshot ORM model works
5 - test_compute_and_persist_snapshot β€” _compute_and_persist_profile_snapshot writes a row
6 - test_snapshot_is_read_by_profile_route β€” GET /handle serves data from snapshot (no live queries)
7 - test_stale_snapshot_triggers_fallback β€” is_stale=True causes live fallback
8 - test_missing_snapshot_triggers_fallback β€” missing row causes live fallback
9 - test_enqueue_profile_snapshot β€” enqueue_profile_snapshot inserts a pending job
10 - test_push_enqueues_profile_snapshot β€” musehub_wire enqueues profile.snapshot on push
11 - test_profile_snapshot_provider_returns_empty β€” ProfileSnapshotProvider.compute() returns []
12 """
13 from __future__ import annotations
14
15 import json
16 from datetime import datetime, timezone
17
18 import pytest
19 from httpx import AsyncClient
20 from sqlalchemy import select
21 from sqlalchemy.ext.asyncio import AsyncSession
22
23 from muse.core.types import now_utc_iso
24 from musehub.db.musehub_identity_models import MusehubIdentity, MusehubProfileSnapshot
25 from musehub.db.musehub_jobs_models import MusehubBackgroundJob
26 from musehub.db.musehub_repo_models import MusehubRepo
27 from musehub.types.json_types import JSONObject
28
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34
35 async def _seed_identity(
36 db: AsyncSession,
37 *,
38 handle: str = "snapuser",
39 user_id: str = "snap-user-001",
40 ) -> MusehubIdentity:
41 identity = MusehubIdentity(
42 identity_id=user_id,
43 handle=handle,
44 identity_type="human",
45 bio="Snapshot test bio",
46 avatar_url=None,
47 )
48 db.add(identity)
49 await db.commit()
50 await db.refresh(identity)
51 return identity
52
53
54 async def _seed_repo(
55 db: AsyncSession,
56 *,
57 owner: str = "snapuser",
58 owner_user_id: str = "snap-user-001",
59 slug: str = "snap-repo",
60 ) -> MusehubRepo:
61 from musehub.core.genesis import compute_repo_id
62 repo_id = compute_repo_id(owner_user_id, slug, "code", now_utc_iso())
63 repo = MusehubRepo(
64 repo_id=repo_id,
65 name=slug,
66 owner=owner,
67 slug=slug,
68 visibility="public",
69 owner_user_id=owner_user_id,
70 )
71 db.add(repo)
72 await db.commit()
73 await db.refresh(repo)
74 return repo
75
76
77 async def _seed_snapshot(
78 db: AsyncSession,
79 *,
80 handle: str = "snapuser",
81 stats: JSONObject | None = None,
82 is_stale: bool = False,
83 ) -> MusehubProfileSnapshot:
84 data = {
85 "stats": stats or {"repo_count": 3, "commit_count": 42, "agent_count": 2, "avg_health": None},
86 "repos": [],
87 "heatmap": {"days": [], "total": 0, "longest_streak": 0, "current_streak": 0},
88 "agent_fleet": [],
89 "badges": [],
90 "footprint": [],
91 "activity_canvas": [],
92 }
93 snap = MusehubProfileSnapshot(
94 handle=handle,
95 data_json=json.dumps(data),
96 computed_at=datetime.now(tz=timezone.utc),
97 is_stale=is_stale,
98 )
99 db.add(snap)
100 await db.commit()
101 await db.refresh(snap)
102 return snap
103
104
105 # ---------------------------------------------------------------------------
106 # Phase 1 β€” ORM model
107 # ---------------------------------------------------------------------------
108
109
110 async def test_snapshot_table_is_queryable(db_session: AsyncSession) -> None:
111 """MusehubProfileSnapshot ORM model persists and queries correctly."""
112 snap = MusehubProfileSnapshot(
113 handle="tabletest",
114 data_json='{"stats": {"repo_count": 1}}',
115 computed_at=datetime.now(tz=timezone.utc),
116 is_stale=False,
117 )
118 db_session.add(snap)
119 await db_session.commit()
120
121 result = await db_session.execute(
122 select(MusehubProfileSnapshot).where(MusehubProfileSnapshot.handle == "tabletest")
123 )
124 row = result.scalar_one_or_none()
125 assert row is not None
126 assert row.handle == "tabletest"
127 assert row.is_stale is False
128 data = json.loads(row.data_json)
129 assert data["stats"]["repo_count"] == 1
130
131
132 # ---------------------------------------------------------------------------
133 # Phase 2 β€” ProfileSnapshotProvider
134 # ---------------------------------------------------------------------------
135
136
137 async def test_profile_snapshot_provider_returns_empty(db_session: AsyncSession) -> None:
138 """ProfileSnapshotProvider.compute() always returns [] (writes directly to table)."""
139 from musehub.services.musehub_intel_providers import ProfileSnapshotProvider
140 from musehub.core.genesis import compute_repo_id
141
142 await _seed_identity(db_session)
143 repo = await _seed_repo(db_session)
144
145 provider = ProfileSnapshotProvider()
146 result = await provider.compute(
147 db_session,
148 repo.repo_id,
149 "",
150 {"handle": "snapuser"},
151 )
152 assert result == []
153
154
155 async def test_compute_and_persist_snapshot(db_session: AsyncSession) -> None:
156 """_compute_and_persist_profile_snapshot writes a row to musehub_profile_snapshots."""
157 from musehub.services.musehub_intel_providers import _compute_and_persist_profile_snapshot
158
159 await _seed_identity(db_session)
160 await _seed_repo(db_session)
161
162 await _compute_and_persist_profile_snapshot(db_session, "snapuser")
163 await db_session.commit()
164
165 result = await db_session.execute(
166 select(MusehubProfileSnapshot).where(MusehubProfileSnapshot.handle == "snapuser")
167 )
168 row = result.scalar_one_or_none()
169 assert row is not None
170 assert row.is_stale is False
171 data = json.loads(row.data_json)
172 assert "stats" in data
173 assert "repos" in data
174 assert "heatmap" in data
175 assert "badges" in data
176 assert "activity_canvas" in data
177
178
179 async def test_compute_and_persist_snapshot_upserts(db_session: AsyncSession) -> None:
180 """Re-running _compute_and_persist_profile_snapshot overwrites the existing row."""
181 from musehub.services.musehub_intel_providers import _compute_and_persist_profile_snapshot
182
183 await _seed_identity(db_session)
184 await _seed_repo(db_session)
185
186 # First write
187 await _compute_and_persist_profile_snapshot(db_session, "snapuser")
188 await db_session.commit()
189
190 # Second write β€” should not raise and should overwrite
191 await _compute_and_persist_profile_snapshot(db_session, "snapuser")
192 await db_session.commit()
193
194 result = await db_session.execute(
195 select(MusehubProfileSnapshot).where(MusehubProfileSnapshot.handle == "snapuser")
196 )
197 rows = result.scalars().all()
198 assert len(rows) == 1 # upsert, not insert
199
200
201 async def test_compute_and_persist_snapshot_missing_identity(db_session: AsyncSession) -> None:
202 """_compute_and_persist_profile_snapshot is a no-op for unknown handles."""
203 from musehub.services.musehub_intel_providers import _compute_and_persist_profile_snapshot
204
205 # No identity seeded β€” should not raise
206 await _compute_and_persist_profile_snapshot(db_session, "nobody-exists-xyz")
207 await db_session.commit()
208
209 result = await db_session.execute(
210 select(MusehubProfileSnapshot).where(MusehubProfileSnapshot.handle == "nobody-exists-xyz")
211 )
212 assert result.scalar_one_or_none() is None
213
214
215 # ---------------------------------------------------------------------------
216 # Phase 3 β€” enqueue_profile_snapshot
217 # ---------------------------------------------------------------------------
218
219
220 async def test_enqueue_profile_snapshot(db_session: AsyncSession) -> None:
221 """enqueue_profile_snapshot inserts a pending profile.snapshot job."""
222 from musehub.services.musehub_jobs import enqueue_profile_snapshot
223
224 await _seed_identity(db_session)
225 repo = await _seed_repo(db_session)
226
227 job_id = await enqueue_profile_snapshot(db_session, repo.repo_id, "snapuser")
228 await db_session.commit()
229
230 assert job_id is not None
231
232 result = await db_session.execute(
233 select(MusehubBackgroundJob).where(MusehubBackgroundJob.job_id == job_id)
234 )
235 job = result.scalar_one_or_none()
236 assert job is not None
237 assert job.job_type == "profile.snapshot"
238 assert job.status == "pending"
239 payload = job.payload or {}
240 assert payload.get("handle") == "snapuser"
241
242
243 async def test_enqueue_profile_snapshot_is_idempotent(db_session: AsyncSession) -> None:
244 """enqueue_profile_snapshot returns None if a pending job already exists."""
245 from musehub.services.musehub_jobs import enqueue_profile_snapshot
246
247 await _seed_identity(db_session)
248 repo = await _seed_repo(db_session)
249
250 first = await enqueue_profile_snapshot(db_session, repo.repo_id, "snapuser")
251 await db_session.commit()
252 second = await enqueue_profile_snapshot(db_session, repo.repo_id, "snapuser")
253 await db_session.commit()
254
255 assert first is not None
256 assert second is None # idempotent β€” no duplicate
257
258
259 # ---------------------------------------------------------------------------
260 # Phase 4 β€” SSR snapshot fast-path
261 # ---------------------------------------------------------------------------
262
263
264 async def test_snapshot_is_read_by_profile_route(
265 client: AsyncClient,
266 db_session: AsyncSession,
267 ) -> None:
268 """GET /handle serves stats from the pre-computed snapshot when present."""
269 await _seed_identity(db_session, handle="snapuser2", user_id="snap-user-002")
270 await _seed_snapshot(
271 db_session,
272 handle="snapuser2",
273 stats={"repo_count": 99, "commit_count": 777, "agent_count": 5, "avg_health": None},
274 )
275
276 resp = await client.get("/snapuser2?format=json")
277 assert resp.status_code == 200
278 body = resp.json()
279 # The JSON route serialises from the stats dict
280 assert body["repoCount"] == 99
281 assert body["commitCount"] == 777
282
283
284 async def test_stale_snapshot_triggers_fallback(
285 client: AsyncClient,
286 db_session: AsyncSession,
287 ) -> None:
288 """is_stale=True snapshot is ignored; live computation runs instead."""
289 await _seed_identity(db_session, handle="staleuser", user_id="stale-001")
290 await _seed_snapshot(
291 db_session,
292 handle="staleuser",
293 stats={"repo_count": 999, "commit_count": 9999, "agent_count": 0, "avg_health": None},
294 is_stale=True,
295 )
296
297 resp = await client.get("/staleuser?format=json")
298 assert resp.status_code == 200
299 body = resp.json()
300 # Live computation returns 0 repos (no repos seeded), not the stale 999
301 assert body["repoCount"] == 0
302
303
304 async def test_missing_snapshot_falls_back_to_live(
305 client: AsyncClient,
306 db_session: AsyncSession,
307 ) -> None:
308 """When no snapshot exists the route computes live and returns valid data."""
309 await _seed_identity(db_session, handle="nosnapuser", user_id="nosnap-001")
310
311 resp = await client.get("/nosnapuser?format=json")
312 assert resp.status_code == 200
313 body = resp.json()
314 assert body["handle"] == "nosnapuser"
315 assert body["repoCount"] == 0
316
317
318 async def test_snapshot_html_route_serves_200(
319 client: AsyncClient,
320 db_session: AsyncSession,
321 ) -> None:
322 """Profile HTML route works with a pre-computed snapshot."""
323 await _seed_identity(db_session, handle="htmlsnap", user_id="htmlsnap-001")
324 await _seed_snapshot(db_session, handle="htmlsnap")
325
326 resp = await client.get("/htmlsnap")
327 assert resp.status_code == 200
328 assert "text/html" in resp.headers["content-type"]