"""TDD spec — Phase 3: data-dense symbol rows, no gradients. Changes ─────── 1. _fetch_symbol_list returns ``weekly`` (list[int]) per row — sparkline data 2. Template: each row gets a sub-line (.sym2-row-meta) with: coupling count (when > 0) · age from first_introduced · heat count 3. CSS: gradients removed from heat fill and hero background — solid colors only 4. Hero title: gradient-text span dropped, plain color instead Tier breakdown ────────────── T301 _fetch_symbol_list returns weekly list per row T302 Page renders 200 OK with sym2-row-meta in HTML T303 sym2-row-meta includes coupling chip when coupling_count > 0 T304 sym2-row-meta omits coupling chip when coupling_count == 0 T305 sym2-row-meta includes age label when first_introduced present T306 HTML has no gradient-text on hero title (phased out) T307 Heat fill class has no gradient in SCSS source T308 Hero ::before has no radial-gradient in SCSS source """ from __future__ import annotations import datetime as _dt import secrets from datetime import timezone from pathlib import Path import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from musehub.db.musehub_intel_models import MusehubSymbolIntel, MusehubSymbolVitals from muse.core.types import blob_id, long_id from tests.factories import create_repo SCSS_SYMBOLS = Path(__file__).parents[1] / "src/scss/components/_symbols.scss" def _now() -> _dt.datetime: return _dt.datetime.now(tz=timezone.utc) def _cid() -> str: return blob_id(secrets.token_bytes(32)) def _lid() -> str: return long_id(secrets.token_hex(32)) async def _seed_symbol( session: AsyncSession, repo_id: str, address: str, *, coupling_count: int = 0, first_introduced: _dt.datetime | None = None, weekly: list[int] | None = None, ) -> None: intel = MusehubSymbolIntel( repo_id=repo_id, address=address, churn=len(weekly) if weekly else 3, churn_30d=3, churn_90d=3, blast=0, blast_direct=0, blast_cross=0, blast_top=[], last_changed=_now(), author_count=1, gravity=0.0, weekly=weekly or [0, 1, 2, 1, 0, 3, 1], last_commit_id=_lid(), op="insert", ) session.add(intel) vitals = MusehubSymbolVitals( repo_id=repo_id, address=address, first_introduced=first_introduced or _now(), change_count=3, version_count=1, op_add=1, op_modify=0, op_delete=0, op_move=0, coupling_count=coupling_count, ) session.add(vitals) await session.flush() # --------------------------------------------------------------------------- # T301 — _fetch_symbol_list returns weekly list per row # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t301_fetch_returns_weekly(db_session: AsyncSession) -> None: """_fetch_symbol_list must include a weekly list on each row.""" from musehub.api.routes.musehub.ui_symbols import _fetch_symbol_list repo = await create_repo(db_session, owner="gabriel") await _seed_symbol( db_session, repo.repo_id, "src/a.py::fn", weekly=[0, 1, 2, 3, 2, 1, 0], ) await db_session.flush() symbols, _, _ = await _fetch_symbol_list( db_session, repo.repo_id, q=None, op=[], cursor=None, per_page=50 ) sym = next((s for s in symbols if s["address"] == "src/a.py::fn"), None) assert sym is not None assert "weekly" in sym assert isinstance(sym["weekly"], list) assert sym["weekly"] == [0, 1, 2, 3, 2, 1, 0] # --------------------------------------------------------------------------- # T302 — page renders 200 with sym2-row-meta in HTML # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t302_page_renders_row_meta( client: AsyncClient, db_session: AsyncSession, ) -> None: """Symbol list page must render sym2-row elements in the table.""" repo = await create_repo(db_session, owner="gabriel") await _seed_symbol(db_session, repo.repo_id, "src/b.py::fn_meta") await db_session.commit() resp = await client.get(f"/gabriel/{repo.slug}/symbols") assert resp.status_code == 200 assert "sym2-row" in resp.text # --------------------------------------------------------------------------- # T303 — coupling chip visible when coupling_count > 0 # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t303_coupling_chip_when_coupled( client: AsyncClient, db_session: AsyncSession, ) -> None: """When coupling_count > 0, a sym2-num element appears in the coupling column.""" repo = await create_repo(db_session, owner="gabriel") await _seed_symbol( db_session, repo.repo_id, "src/c.py::coupled_fn", coupling_count=5, ) await db_session.commit() resp = await client.get(f"/gabriel/{repo.slug}/symbols") assert resp.status_code == 200 assert "sym2-td-coupled" in resp.text assert "sym2-num" in resp.text # --------------------------------------------------------------------------- # T304 — coupling chip absent when coupling_count == 0 # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t304_no_coupling_chip_when_isolated( client: AsyncClient, db_session: AsyncSession, ) -> None: """When coupling_count == 0, no coupling chip must appear.""" repo = await create_repo(db_session, owner="gabriel") await _seed_symbol( db_session, repo.repo_id, "src/d.py::solo_fn", coupling_count=0, ) await db_session.commit() resp = await client.get(f"/gabriel/{repo.slug}/symbols") assert resp.status_code == 200 assert "sym2-coupling-chip" not in resp.text # --------------------------------------------------------------------------- # T305 — age label present when first_introduced available # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t305_age_label_present( client: AsyncClient, db_session: AsyncSession, ) -> None: """A sym2-td-date column must appear in every symbol row.""" repo = await create_repo(db_session, owner="gabriel") introduced = _dt.datetime(2024, 6, 1, tzinfo=timezone.utc) await _seed_symbol( db_session, repo.repo_id, "src/e.py::old_fn", first_introduced=introduced, ) await db_session.commit() resp = await client.get(f"/gabriel/{repo.slug}/symbols") assert resp.status_code == 200 assert "sym2-td-date" in resp.text # --------------------------------------------------------------------------- # T306 — hero title has no gradient-text class # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t306_no_gradient_text_on_hero( client: AsyncClient, db_session: AsyncSession, ) -> None: """The symbols page hero title must not use gradient-text.""" repo = await create_repo(db_session, owner="gabriel") await db_session.commit() resp = await client.get(f"/gabriel/{repo.slug}/symbols") assert resp.status_code == 200 # The hero title h1 must not contain gradient-text # Simple heuristic: the phrase appears only in non-h1 context if at all import re h1_blocks = re.findall(r"