gabriel / musehub public

test_repo_card_e2e.py file-level

at sha256:2 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:1 Merge 'fix/test-suite-real-bugs' into 'dev' β€” proposal: fix: full test … · gabriel · Sep 7, 2026
1 """
2 Tier 3 β€” E2E (SSR) tests for the enriched repo card component.
3
4 These tests exercise the full HTTP path: a real ASGI client hits the domain
5 detail route, the route calls enrich_repo_cards(), and we assert the rendered
6 HTML contains the expected enrichment signals. No JS execution β€” all signals
7 must be server-side rendered.
8
9 Test IDs
10 --------
11 T300 β€” domain detail page returns 200 and contains rc-card markup
12 T301 β€” pulse sparkline SVG is rendered for repos with commits
13 T302 β€” health badge class matches actual health signal in HTML
14 T303 β€” autonomy stat renders correct percentage for an all-agent repo
15 T304 β€” hottest symbol name appears in rc-intel row
16 T305 β€” blast leader name appears in rc-intel row
17 T306 β€” repos with no intel data render clean badge and zero autonomy
18 T307 β€” ?format=json response is unaffected by enrichment (no crash)
19 """
20 from __future__ import annotations
21
22 import secrets
23 from datetime import datetime, timedelta, timezone
24
25 import pytest
26 from httpx import AsyncClient
27 from sqlalchemy.ext.asyncio import AsyncSession
28 from sqlalchemy import text
29
30 from musehub.db.musehub_domain_models import MusehubDomain
31 from musehub.db.musehub_intel_models import MusehubIntelDead, MusehubSymbolIntel
32 from musehub.db.musehub_repo_models import MusehubRepo
33 from musehub.core.genesis import compute_identity_id, compute_repo_id
34 from tests.factories import create_commit, create_repo
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41 def _utc_now() -> datetime:
42 return datetime.now(tz=timezone.utc)
43
44
45 def _domain_id() -> str:
46 return f"sha256:{secrets.token_hex(32)}"
47
48
49 async def _make_domain(
50 db: AsyncSession,
51 *,
52 author_slug: str = "testauthor",
53 slug: str = "testdomain",
54 display_name: str = "Test Domain",
55 ) -> MusehubDomain:
56 """Seed a MusehubDomain and return it."""
57 domain = MusehubDomain(
58 domain_id=_domain_id(),
59 author_slug=author_slug,
60 slug=slug,
61 display_name=display_name,
62 description="A test domain",
63 version="0.1.0",
64 viewer_type="code",
65 capabilities={
66 "dimensions": [{"name": "symbol", "description": "Symbol dimension"}],
67 "kinds": ["function"],
68 "merge_semantics": "ot",
69 },
70 )
71 db.add(domain)
72 await db.commit()
73 await db.refresh(domain)
74 return domain
75
76
77 async def _attach_repo_to_domain(
78 db: AsyncSession,
79 repo: MusehubRepo,
80 domain: MusehubDomain,
81 ) -> None:
82 """Link a repo to a marketplace domain via marketplace_domain_id.
83
84 Must NOT set the plain ``domain_id`` column β€” that's an unrelated
85 VCS-plugin category string ("code"/"midi"/"mist"), never the marketplace
86 domain link. See musehub_domains.list_repos_for_domain's docstring.
87 """
88 await db.execute(
89 text("UPDATE musehub_repos SET marketplace_domain_id = :did WHERE repo_id = :rid"),
90 {"did": domain.domain_id, "rid": repo.repo_id},
91 )
92 await db.commit()
93
94
95 async def _make_public_repo(
96 db: AsyncSession,
97 *,
98 owner: str = "testowner",
99 slug: str | None = None,
100 ) -> MusehubRepo:
101 """Seed a public repo using the factory helper."""
102 repo = await create_repo(db, visibility="public")
103 if slug:
104 # Patch slug for readable assertions
105 await db.execute(
106 text("UPDATE musehub_repos SET slug = :s, owner = :o WHERE repo_id = :rid"),
107 {"s": slug, "o": owner, "rid": repo.repo_id},
108 )
109 await db.commit()
110 await db.refresh(repo)
111 return repo
112
113
114 async def _add_agent_commit(db: AsyncSession, repo_id: str) -> None:
115 """Insert a commit with agent_id set."""
116 commit = await create_commit(db, repo_id, timestamp=_utc_now())
117 await db.execute(
118 text("UPDATE musehub_commits SET agent_id = 'claude-code' WHERE commit_id = :cid"),
119 {"cid": commit.commit_id},
120 )
121 await db.commit()
122
123
124 async def _insert_symbol_intel(
125 db: AsyncSession,
126 repo_id: str,
127 address: str,
128 churn_30d: int = 0,
129 blast: int = 0,
130 ) -> None:
131 row = MusehubSymbolIntel(
132 repo_id=repo_id, address=address, churn_30d=churn_30d, blast=blast
133 )
134 db.add(row)
135 await db.commit()
136
137
138 async def _insert_dead(db: AsyncSession, repo_id: str, address: str) -> None:
139 from musehub.db.musehub_intel_models import MusehubIntelDead
140 row = MusehubIntelDead(
141 repo_id=repo_id,
142 address=address,
143 kind="function",
144 confidence="high",
145 ref="main",
146 )
147 db.add(row)
148 await db.commit()
149
150
151 def _domain_url(author_slug: str, slug: str) -> str:
152 return f"/domains/@{author_slug}/{slug}"
153
154
155 # ---------------------------------------------------------------------------
156 # T300 β€” page returns 200 with rc-card markup
157 # ---------------------------------------------------------------------------
158
159 @pytest.mark.asyncio
160 async def test_t300_domain_detail_returns_rc_cards(
161 client: AsyncClient,
162 db_session: AsyncSession,
163 ) -> None:
164 """T300: GET /domains/@author/slug returns 200 and renders rc-card elements."""
165 domain = await _make_domain(db_session)
166 repo = await _make_public_repo(db_session)
167 await _attach_repo_to_domain(db_session, repo, domain)
168
169 resp = await client.get(_domain_url(domain.author_slug, domain.slug))
170 assert resp.status_code == 200
171 assert "text/html" in resp.headers["content-type"]
172 assert "rc-card" in resp.text
173
174
175 # ---------------------------------------------------------------------------
176 # T301 β€” sparkline SVG rendered when commits exist
177 # ---------------------------------------------------------------------------
178
179 @pytest.mark.asyncio
180 async def test_t301_sparkline_rendered_for_repo_with_commits(
181 client: AsyncClient,
182 db_session: AsyncSession,
183 ) -> None:
184 """T301: a repo with recent commits renders a <svg class="rc-sparkline"> element."""
185 domain = await _make_domain(db_session, slug="sparktest")
186 repo = await _make_public_repo(db_session)
187 await _attach_repo_to_domain(db_session, repo, domain)
188 await create_commit(db_session, repo.repo_id, timestamp=_utc_now())
189
190 resp = await client.get(_domain_url(domain.author_slug, domain.slug))
191 assert resp.status_code == 200
192 assert 'class="rc-sparkline"' in resp.text
193
194
195 # ---------------------------------------------------------------------------
196 # T302 β€” health badge class matches signal
197 # ---------------------------------------------------------------------------
198
199 @pytest.mark.asyncio
200 async def test_t302_health_badge_risk_when_errors(
201 client: AsyncClient,
202 db_session: AsyncSession,
203 ) -> None:
204 """T302: health badge gauge aria-label is "risk" when breakage errors exist."""
205 from musehub.db.musehub_intel_models import MusehubIntelBreakageMeta
206 domain = await _make_domain(db_session, slug="healthtest")
207 repo = await _make_public_repo(db_session)
208 await _attach_repo_to_domain(db_session, repo, domain)
209
210 db_session.add(MusehubIntelBreakageMeta(
211 repo_id=repo.repo_id,
212 total_issues=3,
213 error_count=3,
214 warning_count=0,
215 file_count=1,
216 ref="main",
217 ))
218 await db_session.commit()
219
220 resp = await client.get(_domain_url(domain.author_slug, domain.slug))
221 assert resp.status_code == 200
222 assert 'aria-label="risk"' in resp.text
223
224
225 @pytest.mark.asyncio
226 async def test_t302b_health_badge_warn_when_dead(
227 client: AsyncClient,
228 db_session: AsyncSession,
229 ) -> None:
230 """T302b: health badge gauge aria-label is "warn" when dead symbols exist."""
231 domain = await _make_domain(db_session, slug="warntest")
232 repo = await _make_public_repo(db_session)
233 await _attach_repo_to_domain(db_session, repo, domain)
234 await _insert_dead(db_session, repo.repo_id, "src/old.py::stale_fn")
235
236 resp = await client.get(_domain_url(domain.author_slug, domain.slug))
237 assert resp.status_code == 200
238 assert 'aria-label="warn"' in resp.text
239
240
241 # ---------------------------------------------------------------------------
242 # T303 β€” autonomy percentage rendered correctly
243 # ---------------------------------------------------------------------------
244
245 @pytest.mark.asyncio
246 async def test_t303_autonomy_pct_rendered_for_all_agent_repo(
247 client: AsyncClient,
248 db_session: AsyncSession,
249 ) -> None:
250 """T303: a repo with only agent commits renders '100%' in the autonomy stat."""
251 domain = await _make_domain(db_session, slug="autonomytest")
252 repo = await _make_public_repo(db_session)
253 await _attach_repo_to_domain(db_session, repo, domain)
254
255 for _ in range(3):
256 await _add_agent_commit(db_session, repo.repo_id)
257
258 resp = await client.get(_domain_url(domain.author_slug, domain.slug))
259 assert resp.status_code == 200
260 assert "100%" in resp.text
261 assert "autonomy" in resp.text
262
263
264 # ---------------------------------------------------------------------------
265 # T304 β€” hottest symbol name in rc-intel row
266 # ---------------------------------------------------------------------------
267
268 @pytest.mark.asyncio
269 async def test_t304_hottest_symbol_rendered(
270 client: AsyncClient,
271 db_session: AsyncSession,
272 ) -> None:
273 """T304: the hottest symbol's short name appears in an rc-intel row."""
274 domain = await _make_domain(db_session, slug="hottesttest")
275 repo = await _make_public_repo(db_session)
276 await _attach_repo_to_domain(db_session, repo, domain)
277 await _insert_symbol_intel(
278 db_session, repo.repo_id, "src/core.py::compute_totals", churn_30d=42
279 )
280
281 resp = await client.get(_domain_url(domain.author_slug, domain.slug))
282 assert resp.status_code == 200
283 assert "compute_totals" in resp.text
284 assert "hottest" in resp.text
285
286
287 # ---------------------------------------------------------------------------
288 # T305 β€” blast leader name in rc-intel row
289 # ---------------------------------------------------------------------------
290
291 @pytest.mark.asyncio
292 async def test_t305_blast_leader_rendered(
293 client: AsyncClient,
294 db_session: AsyncSession,
295 ) -> None:
296 """T305: the blast leader's short name appears in an rc-intel row."""
297 domain = await _make_domain(db_session, slug="blasttest")
298 repo = await _make_public_repo(db_session)
299 await _attach_repo_to_domain(db_session, repo, domain)
300 await _insert_symbol_intel(
301 db_session, repo.repo_id, "src/api.py::dispatch_event", blast=512
302 )
303
304 resp = await client.get(_domain_url(domain.author_slug, domain.slug))
305 assert resp.status_code == 200
306 assert "dispatch_event" in resp.text
307 assert "blast" in resp.text
308
309
310 # ---------------------------------------------------------------------------
311 # T306 β€” clean / zero enrichment for repo with no intel
312 # ---------------------------------------------------------------------------
313
314 @pytest.mark.asyncio
315 async def test_t306_clean_card_when_no_intel(
316 client: AsyncClient,
317 db_session: AsyncSession,
318 ) -> None:
319 """T306: a repo with zero intel data renders gauge aria-label="clean", no crash."""
320 domain = await _make_domain(db_session, slug="cleantest")
321 repo = await _make_public_repo(db_session)
322 await _attach_repo_to_domain(db_session, repo, domain)
323
324 resp = await client.get(_domain_url(domain.author_slug, domain.slug))
325 assert resp.status_code == 200
326 assert 'aria-label="clean"' in resp.text
327 # No intel rows β†’ no hottest/blast section rendered
328 assert "hottest" not in resp.text
329
330
331 # ---------------------------------------------------------------------------
332 # T307 β€” ?format=json unaffected by enrichment
333 # ---------------------------------------------------------------------------
334
335 @pytest.mark.asyncio
336 async def test_t307_json_format_not_broken_by_enrichment(
337 client: AsyncClient,
338 db_session: AsyncSession,
339 ) -> None:
340 """T307: ?format=json still returns valid JSON after enrichment was wired up."""
341 domain = await _make_domain(db_session, slug="jsontest")
342 repo = await _make_public_repo(db_session)
343 await _attach_repo_to_domain(db_session, repo, domain)
344
345 resp = await client.get(
346 _domain_url(domain.author_slug, domain.slug),
347 params={"format": "json"},
348 )
349 assert resp.status_code == 200
350 assert resp.headers["content-type"].startswith("application/json")
351 data = resp.json()
352 assert "domain" in data
353 assert "repos" in data