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