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