"""Tier 2 — Integration tests for the clone browser UI routes (issue #17). Exercises ``intel_clones_page`` and ``intel_clones_detail_page`` against a real PostgreSQL test database. All 15 cases use the ``client`` + ``db_session`` fixtures from conftest so tests run against the full ASGI stack with live SQL. Cases: I01 List page returns 200 when repo has 5 clusters I02 List page returns 200 + empty state when no clusters I03 Tier filter ``exact`` returns only exact clusters I04 Tier filter ``near`` returns only near clusters I05 Invalid tier coerces to all — 200, no 400 I06 ``top=50`` activates the 50 pill I07 ``top=9999`` clamps to default (20) — 200 I08 Detail page returns 200 for known cluster_hash I09 Detail page returns 200 + empty state for unknown hash I10 Detail page returns 200 + empty state when cluster param absent I11 Dashboard returns 200 with clones card when rows exist I12 Dashboard returns 200 with empty card when no rows I13 Detail page members grouped by file → files_breakdown present I14 Cross-file cluster → ``cl-cross-file`` badge in response I15 Same-file cluster → ``cl-cross-file`` absent from response """ from __future__ import annotations import json import pytest import pytest_asyncio import sqlalchemy as sa from httpx import AsyncClient from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession from musehub.db.musehub_intel_models import MusehubIntelClones from musehub.db.musehub_repo_models import MusehubRepo from tests.factories import create_repo from muse.core.types import long_id # ───────────────────────────────────────────────────────────────────────────── # Seed helpers # ───────────────────────────────────────────────────────────────────────────── _REF = long_id("a" * 64) def _members( file_a: str = "src/a.py", file_b: str | None = "src/b.py", n: int = 2, kind: str = "function", language: str = "Python", ) -> str: """Build a members_json blob for test fixture rows.""" members = [] for i in range(n): file = file_a if (file_b is None or i % 2 == 0) else file_b members.append( { "address": f"{file}::fn_{i}", "kind": kind, "language": language, "body_hash": long_id("b" * 64), "signature_id": long_id("c" * 64), "content_id": long_id("d" * 64), } ) return json.dumps(members) def _same_file_members(n: int = 2) -> str: """All members in one file — not cross-file.""" return _members(file_a="src/a.py", file_b=None, n=n) async def _insert_cluster( session: AsyncSession, repo_id: str, *, cluster_hash: str, tier: str = "exact", member_count: int = 2, members_json: str | None = None, ) -> None: """Upsert a single MusehubIntelClones row.""" if members_json is None: members_json = _members(n=member_count) await session.execute( pg_insert(MusehubIntelClones) .values( repo_id=repo_id, cluster_hash=cluster_hash, tier=tier, member_count=member_count, members_json=members_json, ref=_REF, ) .on_conflict_do_update( index_elements=["repo_id", "cluster_hash"], set_={"tier": tier, "member_count": member_count, "members_json": members_json}, ) ) await session.commit() # ───────────────────────────────────────────────────────────────────────────── # Fixtures # ───────────────────────────────────────────────────────────────────────────── @pytest_asyncio.fixture async def repo(db_session: AsyncSession) -> MusehubRepo: """A public repo owned by testclones for all integration tests.""" return await create_repo(db_session, owner="testclones", slug="clone-browser") @pytest_asyncio.fixture async def repo_with_clusters(db_session: AsyncSession, repo: MusehubRepo) -> MusehubRepo: """Seed 3 exact + 2 near clusters for the list-page tests.""" for i in range(3): await _insert_cluster( db_session, str(repo.repo_id), cluster_hash=f"sha256:exact{i:060d}", tier="exact", member_count=i + 2, ) for i in range(2): await _insert_cluster( db_session, str(repo.repo_id), cluster_hash=f"sha256:near{i:061d}", tier="near", member_count=i + 4, ) return repo # ───────────────────────────────────────────────────────────────────────────── # I01–I07 — List page # ───────────────────────────────────────────────────────────────────────────── class TestClonesListPage: """Integration tests for GET /{owner}/{repo_slug}/intel/clones.""" @pytest.mark.asyncio async def test_I01_list_200_with_clusters( self, client: AsyncClient, repo_with_clusters: MusehubRepo ) -> None: """Seeded clusters render without error.""" r = await client.get(f"/testclones/clone-browser/intel/clones") assert r.status_code == 200 assert b"cl-row" in r.content @pytest.mark.asyncio async def test_I02_list_200_empty_state( self, client: AsyncClient, repo: MusehubRepo ) -> None: """Repo with no clusters renders the empty state at HTTP 200.""" r = await client.get(f"/testclones/clone-browser/intel/clones") assert r.status_code == 200 assert b"intel.code.clones" in r.content @pytest.mark.asyncio async def test_I03_tier_exact_filter( self, client: AsyncClient, repo_with_clusters: MusehubRepo ) -> None: """``?tier=exact`` shows only exact clusters.""" r = await client.get("/testclones/clone-browser/intel/clones?tier=exact") assert r.status_code == 200 body = r.text assert "cl-badge--exact" in body assert "cl-badge--near" not in body @pytest.mark.asyncio async def test_I04_tier_near_filter( self, client: AsyncClient, repo_with_clusters: MusehubRepo ) -> None: """``?tier=near`` shows only near clusters.""" r = await client.get("/testclones/clone-browser/intel/clones?tier=near") assert r.status_code == 200 body = r.text assert "cl-badge--near" in body assert "cl-badge--exact" not in body @pytest.mark.asyncio async def test_I05_invalid_tier_coerces_to_all( self, client: AsyncClient, repo_with_clusters: MusehubRepo ) -> None: """Invalid tier value returns 200 showing all clusters.""" r = await client.get("/testclones/clone-browser/intel/clones?tier=bogus") assert r.status_code == 200 body = r.text assert "cl-badge--exact" in body assert "cl-badge--near" in body @pytest.mark.asyncio async def test_I06_top_50_pill_active( self, client: AsyncClient, repo_with_clusters: MusehubRepo ) -> None: """``?top=50`` activates the 50 filter pill.""" r = await client.get("/testclones/clone-browser/intel/clones?top=50") assert r.status_code == 200 assert b"top=50" in r.content @pytest.mark.asyncio async def test_I07_out_of_range_top_clamps_to_default( self, client: AsyncClient, repo_with_clusters: MusehubRepo ) -> None: """``top=9999`` is not a valid top value; page returns 200 at default.""" r = await client.get("/testclones/clone-browser/intel/clones?top=9999") assert r.status_code == 200 # ───────────────────────────────────────────────────────────────────────────── # I08–I15 — Detail page # ───────────────────────────────────────────────────────────────────────────── class TestClonesDetailPage: """Integration tests for GET /{owner}/{repo_slug}/intel/clones/detail.""" @pytest_asyncio.fixture async def cross_file_cluster(self, db_session: AsyncSession, repo: MusehubRepo) -> str: h = long_id("f" * 64) await _insert_cluster( db_session, str(repo.repo_id), cluster_hash=h, tier="exact", member_count=4, members_json=_members(file_a="src/a.py", file_b="src/b.py", n=4), ) return h @pytest_asyncio.fixture async def same_file_cluster(self, db_session: AsyncSession, repo: MusehubRepo) -> str: h = long_id("e" * 64) await _insert_cluster( db_session, str(repo.repo_id), cluster_hash=h, tier="near", member_count=3, members_json=_same_file_members(n=3), ) return h @pytest.mark.asyncio async def test_I08_detail_200_known_hash( self, client: AsyncClient, cross_file_cluster: str ) -> None: """Detail page renders at 200 for an existing cluster_hash.""" r = await client.get( f"/testclones/clone-browser/intel/clones/detail" f"?cluster={cross_file_cluster}" ) assert r.status_code == 200 assert b"cl-member-row" in r.content @pytest.mark.asyncio async def test_I09_detail_200_unknown_hash( self, client: AsyncClient, repo: MusehubRepo ) -> None: """Unknown hash renders empty state at HTTP 200, not 404 or 500.""" r = await client.get( "/testclones/clone-browser/intel/clones/detail" "?cluster=sha256:0000000000000000" ) assert r.status_code == 200 assert b"No clone cluster found" in r.content @pytest.mark.asyncio async def test_I10_detail_200_no_cluster_param( self, client: AsyncClient, repo: MusehubRepo ) -> None: """Missing cluster param renders empty state at HTTP 200.""" r = await client.get("/testclones/clone-browser/intel/clones/detail") assert r.status_code == 200 assert b"No cluster specified" in r.content @pytest.mark.asyncio async def test_I11_dashboard_clones_card_with_data( self, client: AsyncClient, db_session: AsyncSession, repo: MusehubRepo ) -> None: """Dashboard card shows cluster count when rows exist.""" await _insert_cluster( db_session, str(repo.repo_id), cluster_hash=long_id("d" * 64), tier="exact", member_count=5, ) r = await client.get("/testclones/clone-browser/intel") assert r.status_code == 200 assert b"CLONES" in r.content @pytest.mark.asyncio async def test_I12_dashboard_clones_card_empty_state( self, client: AsyncClient, repo: MusehubRepo ) -> None: """Dashboard renders clones card empty state without 500 when no rows.""" r = await client.get("/testclones/clone-browser/intel") assert r.status_code == 200 assert b"No clone clusters yet" in r.content @pytest.mark.asyncio async def test_I13_detail_files_breakdown_present( self, client: AsyncClient, cross_file_cluster: str ) -> None: """Detail page renders file breakdown section for cross-file cluster.""" r = await client.get( f"/testclones/clone-browser/intel/clones/detail" f"?cluster={cross_file_cluster}" ) assert r.status_code == 200 assert b"cl-file-row" in r.content @pytest.mark.asyncio async def test_I14_cross_file_badge_present( self, client: AsyncClient, cross_file_cluster: str ) -> None: """Cross-file cluster shows the cl-cross-file badge.""" r = await client.get( f"/testclones/clone-browser/intel/clones/detail" f"?cluster={cross_file_cluster}" ) assert r.status_code == 200 assert b"cl-cross-file" in r.content @pytest.mark.asyncio async def test_I15_same_file_no_cross_file_badge( self, client: AsyncClient, same_file_cluster: str ) -> None: """Same-file cluster does not show the cl-cross-file badge.""" r = await client.get( f"/testclones/clone-browser/intel/clones/detail" f"?cluster={same_file_cluster}" ) assert r.status_code == 200 assert b"cl-cross-file" not in r.content