gabriel / musehub public
test_clones_integration.py python
336 lines 13.0 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 123 days ago
1 """Tier 2 — Integration tests for the clone browser UI routes (issue #17).
2
3 Exercises ``intel_clones_page`` and ``intel_clones_detail_page`` against a
4 real PostgreSQL test database. All 15 cases use the ``client`` + ``db_session``
5 fixtures from conftest so tests run against the full ASGI stack with live SQL.
6
7 Cases:
8 I01 List page returns 200 when repo has 5 clusters
9 I02 List page returns 200 + empty state when no clusters
10 I03 Tier filter ``exact`` returns only exact clusters
11 I04 Tier filter ``near`` returns only near clusters
12 I05 Invalid tier coerces to all — 200, no 400
13 I06 ``top=50`` activates the 50 pill
14 I07 ``top=9999`` clamps to default (20) — 200
15 I08 Detail page returns 200 for known cluster_hash
16 I09 Detail page returns 200 + empty state for unknown hash
17 I10 Detail page returns 200 + empty state when cluster param absent
18 I11 Dashboard returns 200 with clones card when rows exist
19 I12 Dashboard returns 200 with empty card when no rows
20 I13 Detail page members grouped by file → files_breakdown present
21 I14 Cross-file cluster → ``cl-cross-file`` badge in response
22 I15 Same-file cluster → ``cl-cross-file`` absent from response
23 """
24 from __future__ import annotations
25
26 import json
27
28 import pytest
29 import pytest_asyncio
30 import sqlalchemy as sa
31 from httpx import AsyncClient
32 from sqlalchemy.dialects.postgresql import insert as pg_insert
33 from sqlalchemy.ext.asyncio import AsyncSession
34
35 from musehub.db import musehub_models as dbm
36 from tests.factories import create_repo
37 from muse.core.types import long_id
38
39 # ─────────────────────────────────────────────────────────────────────────────
40 # Seed helpers
41 # ─────────────────────────────────────────────────────────────────────────────
42
43 _REF = long_id("a" * 64)
44
45
46 def _members(
47 file_a: str = "src/a.py",
48 file_b: str | None = "src/b.py",
49 n: int = 2,
50 kind: str = "function",
51 language: str = "Python",
52 ) -> str:
53 """Build a members_json blob for test fixture rows."""
54 members = []
55 for i in range(n):
56 file = file_a if (file_b is None or i % 2 == 0) else file_b
57 members.append(
58 {
59 "address": f"{file}::fn_{i}",
60 "kind": kind,
61 "language": language,
62 "body_hash": long_id("b" * 64),
63 "signature_id": long_id("c" * 64),
64 "content_id": long_id("d" * 64),
65 }
66 )
67 return json.dumps(members)
68
69
70 def _same_file_members(n: int = 2) -> str:
71 """All members in one file — not cross-file."""
72 return _members(file_a="src/a.py", file_b=None, n=n)
73
74
75 async def _insert_cluster(
76 session: AsyncSession,
77 repo_id: str,
78 *,
79 cluster_hash: str,
80 tier: str = "exact",
81 member_count: int = 2,
82 members_json: str | None = None,
83 ) -> None:
84 """Upsert a single MusehubIntelClones row."""
85 if members_json is None:
86 members_json = _members(n=member_count)
87 await session.execute(
88 pg_insert(dbm.MusehubIntelClones)
89 .values(
90 repo_id=repo_id,
91 cluster_hash=cluster_hash,
92 tier=tier,
93 member_count=member_count,
94 members_json=members_json,
95 ref=_REF,
96 )
97 .on_conflict_do_update(
98 index_elements=["repo_id", "cluster_hash"],
99 set_={"tier": tier, "member_count": member_count, "members_json": members_json},
100 )
101 )
102 await session.commit()
103
104
105 # ─────────────────────────────────────────────────────────────────────────────
106 # Fixtures
107 # ─────────────────────────────────────────────────────────────────────────────
108
109 @pytest_asyncio.fixture
110 async def repo(db_session: AsyncSession):
111 """A public repo owned by testclones for all integration tests."""
112 return await create_repo(db_session, owner="testclones", slug="clone-browser")
113
114
115 @pytest_asyncio.fixture
116 async def repo_with_clusters(db_session: AsyncSession, repo):
117 """Seed 3 exact + 2 near clusters for the list-page tests."""
118 for i in range(3):
119 await _insert_cluster(
120 db_session,
121 str(repo.repo_id),
122 cluster_hash=f"sha256:exact{i:060d}",
123 tier="exact",
124 member_count=i + 2,
125 )
126 for i in range(2):
127 await _insert_cluster(
128 db_session,
129 str(repo.repo_id),
130 cluster_hash=f"sha256:near{i:061d}",
131 tier="near",
132 member_count=i + 4,
133 )
134 return repo
135
136
137 # ─────────────────────────────────────────────────────────────────────────────
138 # I01–I07 — List page
139 # ─────────────────────────────────────────────────────────────────────────────
140
141 class TestClonesListPage:
142 """Integration tests for GET /{owner}/{repo_slug}/intel/clones."""
143
144 @pytest.mark.asyncio
145 async def test_I01_list_200_with_clusters(
146 self, client: AsyncClient, repo_with_clusters
147 ) -> None:
148 """Seeded clusters render without error."""
149 r = await client.get(f"/testclones/clone-browser/intel/clones")
150 assert r.status_code == 200
151 assert b"cl-row" in r.content
152
153 @pytest.mark.asyncio
154 async def test_I02_list_200_empty_state(
155 self, client: AsyncClient, repo
156 ) -> None:
157 """Repo with no clusters renders the empty state at HTTP 200."""
158 r = await client.get(f"/testclones/clone-browser/intel/clones")
159 assert r.status_code == 200
160 assert b"intel.code.clones" in r.content
161
162 @pytest.mark.asyncio
163 async def test_I03_tier_exact_filter(
164 self, client: AsyncClient, repo_with_clusters
165 ) -> None:
166 """``?tier=exact`` shows only exact clusters."""
167 r = await client.get("/testclones/clone-browser/intel/clones?tier=exact")
168 assert r.status_code == 200
169 body = r.text
170 assert "cl-badge--exact" in body
171 assert "cl-badge--near" not in body
172
173 @pytest.mark.asyncio
174 async def test_I04_tier_near_filter(
175 self, client: AsyncClient, repo_with_clusters
176 ) -> None:
177 """``?tier=near`` shows only near clusters."""
178 r = await client.get("/testclones/clone-browser/intel/clones?tier=near")
179 assert r.status_code == 200
180 body = r.text
181 assert "cl-badge--near" in body
182 assert "cl-badge--exact" not in body
183
184 @pytest.mark.asyncio
185 async def test_I05_invalid_tier_coerces_to_all(
186 self, client: AsyncClient, repo_with_clusters
187 ) -> None:
188 """Invalid tier value returns 200 showing all clusters."""
189 r = await client.get("/testclones/clone-browser/intel/clones?tier=bogus")
190 assert r.status_code == 200
191 body = r.text
192 assert "cl-badge--exact" in body
193 assert "cl-badge--near" in body
194
195 @pytest.mark.asyncio
196 async def test_I06_top_50_pill_active(
197 self, client: AsyncClient, repo_with_clusters
198 ) -> None:
199 """``?top=50`` activates the 50 filter pill."""
200 r = await client.get("/testclones/clone-browser/intel/clones?top=50")
201 assert r.status_code == 200
202 assert b"top=50" in r.content
203
204 @pytest.mark.asyncio
205 async def test_I07_out_of_range_top_clamps_to_default(
206 self, client: AsyncClient, repo_with_clusters
207 ) -> None:
208 """``top=9999`` is not a valid top value; page returns 200 at default."""
209 r = await client.get("/testclones/clone-browser/intel/clones?top=9999")
210 assert r.status_code == 200
211
212
213 # ─────────────────────────────────────────────────────────────────────────────
214 # I08–I15 — Detail page
215 # ─────────────────────────────────────────────────────────────────────────────
216
217 class TestClonesDetailPage:
218 """Integration tests for GET /{owner}/{repo_slug}/intel/clones/detail."""
219
220 @pytest_asyncio.fixture
221 async def cross_file_cluster(self, db_session: AsyncSession, repo):
222 h = long_id("f" * 64)
223 await _insert_cluster(
224 db_session,
225 str(repo.repo_id),
226 cluster_hash=h,
227 tier="exact",
228 member_count=4,
229 members_json=_members(file_a="src/a.py", file_b="src/b.py", n=4),
230 )
231 return h
232
233 @pytest_asyncio.fixture
234 async def same_file_cluster(self, db_session: AsyncSession, repo):
235 h = long_id("e" * 64)
236 await _insert_cluster(
237 db_session,
238 str(repo.repo_id),
239 cluster_hash=h,
240 tier="near",
241 member_count=3,
242 members_json=_same_file_members(n=3),
243 )
244 return h
245
246 @pytest.mark.asyncio
247 async def test_I08_detail_200_known_hash(
248 self, client: AsyncClient, cross_file_cluster
249 ) -> None:
250 """Detail page renders at 200 for an existing cluster_hash."""
251 r = await client.get(
252 f"/testclones/clone-browser/intel/clones/detail"
253 f"?cluster={cross_file_cluster}"
254 )
255 assert r.status_code == 200
256 assert b"cl-member-row" in r.content
257
258 @pytest.mark.asyncio
259 async def test_I09_detail_200_unknown_hash(
260 self, client: AsyncClient, repo
261 ) -> None:
262 """Unknown hash renders empty state at HTTP 200, not 404 or 500."""
263 r = await client.get(
264 "/testclones/clone-browser/intel/clones/detail"
265 "?cluster=sha256:0000000000000000"
266 )
267 assert r.status_code == 200
268 assert b"No clone cluster found" in r.content
269
270 @pytest.mark.asyncio
271 async def test_I10_detail_200_no_cluster_param(
272 self, client: AsyncClient, repo
273 ) -> None:
274 """Missing cluster param renders empty state at HTTP 200."""
275 r = await client.get("/testclones/clone-browser/intel/clones/detail")
276 assert r.status_code == 200
277 assert b"No cluster specified" in r.content
278
279 @pytest.mark.asyncio
280 async def test_I11_dashboard_clones_card_with_data(
281 self, client: AsyncClient, db_session: AsyncSession, repo
282 ) -> None:
283 """Dashboard card shows cluster count when rows exist."""
284 await _insert_cluster(
285 db_session, str(repo.repo_id),
286 cluster_hash=long_id("d" * 64),
287 tier="exact", member_count=5,
288 )
289 r = await client.get("/testclones/clone-browser/intel")
290 assert r.status_code == 200
291 assert b"CLONES" in r.content
292
293 @pytest.mark.asyncio
294 async def test_I12_dashboard_clones_card_empty_state(
295 self, client: AsyncClient, repo
296 ) -> None:
297 """Dashboard renders clones card empty state without 500 when no rows."""
298 r = await client.get("/testclones/clone-browser/intel")
299 assert r.status_code == 200
300 assert b"No clone clusters yet" in r.content
301
302 @pytest.mark.asyncio
303 async def test_I13_detail_files_breakdown_present(
304 self, client: AsyncClient, cross_file_cluster
305 ) -> None:
306 """Detail page renders file breakdown section for cross-file cluster."""
307 r = await client.get(
308 f"/testclones/clone-browser/intel/clones/detail"
309 f"?cluster={cross_file_cluster}"
310 )
311 assert r.status_code == 200
312 assert b"cl-file-row" in r.content
313
314 @pytest.mark.asyncio
315 async def test_I14_cross_file_badge_present(
316 self, client: AsyncClient, cross_file_cluster
317 ) -> None:
318 """Cross-file cluster shows the cl-cross-file badge."""
319 r = await client.get(
320 f"/testclones/clone-browser/intel/clones/detail"
321 f"?cluster={cross_file_cluster}"
322 )
323 assert r.status_code == 200
324 assert b"cl-cross-file" in r.content
325
326 @pytest.mark.asyncio
327 async def test_I15_same_file_no_cross_file_badge(
328 self, client: AsyncClient, same_file_cluster
329 ) -> None:
330 """Same-file cluster does not show the cl-cross-file badge."""
331 r = await client.get(
332 f"/testclones/clone-browser/intel/clones/detail"
333 f"?cluster={same_file_cluster}"
334 )
335 assert r.status_code == 200
336 assert b"cl-cross-file" not in r.content
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 123 days ago