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