test_musehub_profile_snapshot.py
python
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