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