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