test_symbols_v2_p3_template.py
python
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b
Merge branch 'fix/wire-push-external-parent-manifest' into dev
Human
8 days ago
| 1 | """TDD spec — Phase 3: data-dense symbol rows, no gradients. |
| 2 | |
| 3 | Changes |
| 4 | ─────── |
| 5 | 1. _fetch_symbol_list returns ``weekly`` (list[int]) per row — sparkline data |
| 6 | 2. Template: each row gets a sub-line (.sym2-row-meta) with: |
| 7 | coupling count (when > 0) · age from first_introduced · heat count |
| 8 | 3. CSS: gradients removed from heat fill and hero background — solid colors only |
| 9 | 4. Hero title: gradient-text span dropped, plain color instead |
| 10 | |
| 11 | Tier breakdown |
| 12 | ────────────── |
| 13 | T301 _fetch_symbol_list returns weekly list per row |
| 14 | T302 Page renders 200 OK with sym2-row-meta in HTML |
| 15 | T303 sym2-row-meta includes coupling chip when coupling_count > 0 |
| 16 | T304 sym2-row-meta omits coupling chip when coupling_count == 0 |
| 17 | T305 sym2-row-meta includes age label when first_introduced present |
| 18 | T306 HTML has no gradient-text on hero title (phased out) |
| 19 | T307 Heat fill class has no gradient in SCSS source |
| 20 | T308 Hero ::before has no radial-gradient in SCSS source |
| 21 | """ |
| 22 | from __future__ import annotations |
| 23 | |
| 24 | import datetime as _dt |
| 25 | import secrets |
| 26 | from datetime import timezone |
| 27 | from pathlib import Path |
| 28 | |
| 29 | import pytest |
| 30 | from httpx import AsyncClient |
| 31 | from sqlalchemy.ext.asyncio import AsyncSession |
| 32 | |
| 33 | from musehub.db.musehub_intel_models import MusehubSymbolIntel, MusehubSymbolVitals |
| 34 | from muse.core.types import blob_id, long_id |
| 35 | from tests.factories import create_repo |
| 36 | |
| 37 | |
| 38 | SCSS_SYMBOLS = Path(__file__).parents[1] / "src/scss/components/_symbols.scss" |
| 39 | |
| 40 | |
| 41 | def _now() -> _dt.datetime: |
| 42 | return _dt.datetime.now(tz=timezone.utc) |
| 43 | |
| 44 | |
| 45 | def _cid() -> str: |
| 46 | return blob_id(secrets.token_bytes(32)) |
| 47 | |
| 48 | |
| 49 | def _lid() -> str: |
| 50 | return long_id(secrets.token_hex(32)) |
| 51 | |
| 52 | |
| 53 | async def _seed_symbol( |
| 54 | session: AsyncSession, |
| 55 | repo_id: str, |
| 56 | address: str, |
| 57 | *, |
| 58 | coupling_count: int = 0, |
| 59 | first_introduced: _dt.datetime | None = None, |
| 60 | weekly: list[int] | None = None, |
| 61 | ) -> None: |
| 62 | intel = MusehubSymbolIntel( |
| 63 | repo_id=repo_id, |
| 64 | address=address, |
| 65 | churn=len(weekly) if weekly else 3, |
| 66 | churn_30d=3, |
| 67 | churn_90d=3, |
| 68 | blast=0, |
| 69 | blast_direct=0, |
| 70 | blast_cross=0, |
| 71 | blast_top=[], |
| 72 | last_changed=_now(), |
| 73 | author_count=1, |
| 74 | gravity=0.0, |
| 75 | weekly=weekly or [0, 1, 2, 1, 0, 3, 1], |
| 76 | last_commit_id=_lid(), |
| 77 | op="insert", |
| 78 | ) |
| 79 | session.add(intel) |
| 80 | |
| 81 | vitals = MusehubSymbolVitals( |
| 82 | repo_id=repo_id, |
| 83 | address=address, |
| 84 | first_introduced=first_introduced or _now(), |
| 85 | change_count=3, |
| 86 | version_count=1, |
| 87 | op_add=1, |
| 88 | op_modify=0, |
| 89 | op_delete=0, |
| 90 | op_move=0, |
| 91 | coupling_count=coupling_count, |
| 92 | ) |
| 93 | session.add(vitals) |
| 94 | await session.flush() |
| 95 | |
| 96 | |
| 97 | # --------------------------------------------------------------------------- |
| 98 | # T301 — _fetch_symbol_list returns weekly list per row |
| 99 | # --------------------------------------------------------------------------- |
| 100 | |
| 101 | @pytest.mark.asyncio |
| 102 | async def test_t301_fetch_returns_weekly(db_session: AsyncSession) -> None: |
| 103 | """_fetch_symbol_list must include a weekly list on each row.""" |
| 104 | from musehub.api.routes.musehub.ui_symbols import _fetch_symbol_list |
| 105 | |
| 106 | repo = await create_repo(db_session, owner="gabriel") |
| 107 | await _seed_symbol( |
| 108 | db_session, repo.repo_id, "src/a.py::fn", |
| 109 | weekly=[0, 1, 2, 3, 2, 1, 0], |
| 110 | ) |
| 111 | await db_session.flush() |
| 112 | |
| 113 | symbols, _, _ = await _fetch_symbol_list( |
| 114 | db_session, repo.repo_id, q=None, op=[], cursor=None, per_page=50 |
| 115 | ) |
| 116 | sym = next((s for s in symbols if s["address"] == "src/a.py::fn"), None) |
| 117 | assert sym is not None |
| 118 | assert "weekly" in sym |
| 119 | assert isinstance(sym["weekly"], list) |
| 120 | assert sym["weekly"] == [0, 1, 2, 3, 2, 1, 0] |
| 121 | |
| 122 | |
| 123 | # --------------------------------------------------------------------------- |
| 124 | # T302 — page renders 200 with sym2-row-meta in HTML |
| 125 | # --------------------------------------------------------------------------- |
| 126 | |
| 127 | @pytest.mark.asyncio |
| 128 | async def test_t302_page_renders_row_meta( |
| 129 | client: AsyncClient, |
| 130 | db_session: AsyncSession, |
| 131 | ) -> None: |
| 132 | """Symbol list page must render sym2-row elements in the table.""" |
| 133 | repo = await create_repo(db_session, owner="gabriel") |
| 134 | await _seed_symbol(db_session, repo.repo_id, "src/b.py::fn_meta") |
| 135 | await db_session.commit() |
| 136 | |
| 137 | resp = await client.get(f"/gabriel/{repo.slug}/symbols") |
| 138 | assert resp.status_code == 200 |
| 139 | assert "sym2-row" in resp.text |
| 140 | |
| 141 | |
| 142 | # --------------------------------------------------------------------------- |
| 143 | # T303 — coupling chip visible when coupling_count > 0 |
| 144 | # --------------------------------------------------------------------------- |
| 145 | |
| 146 | @pytest.mark.asyncio |
| 147 | async def test_t303_coupling_chip_when_coupled( |
| 148 | client: AsyncClient, |
| 149 | db_session: AsyncSession, |
| 150 | ) -> None: |
| 151 | """When coupling_count > 0, a sym2-num element appears in the coupling column.""" |
| 152 | repo = await create_repo(db_session, owner="gabriel") |
| 153 | await _seed_symbol( |
| 154 | db_session, repo.repo_id, "src/c.py::coupled_fn", |
| 155 | coupling_count=5, |
| 156 | ) |
| 157 | await db_session.commit() |
| 158 | |
| 159 | resp = await client.get(f"/gabriel/{repo.slug}/symbols") |
| 160 | assert resp.status_code == 200 |
| 161 | assert "sym2-td-coupled" in resp.text |
| 162 | assert "sym2-num" in resp.text |
| 163 | |
| 164 | |
| 165 | # --------------------------------------------------------------------------- |
| 166 | # T304 — coupling chip absent when coupling_count == 0 |
| 167 | # --------------------------------------------------------------------------- |
| 168 | |
| 169 | @pytest.mark.asyncio |
| 170 | async def test_t304_no_coupling_chip_when_isolated( |
| 171 | client: AsyncClient, |
| 172 | db_session: AsyncSession, |
| 173 | ) -> None: |
| 174 | """When coupling_count == 0, no coupling chip must appear.""" |
| 175 | repo = await create_repo(db_session, owner="gabriel") |
| 176 | await _seed_symbol( |
| 177 | db_session, repo.repo_id, "src/d.py::solo_fn", |
| 178 | coupling_count=0, |
| 179 | ) |
| 180 | await db_session.commit() |
| 181 | |
| 182 | resp = await client.get(f"/gabriel/{repo.slug}/symbols") |
| 183 | assert resp.status_code == 200 |
| 184 | assert "sym2-coupling-chip" not in resp.text |
| 185 | |
| 186 | |
| 187 | # --------------------------------------------------------------------------- |
| 188 | # T305 — age label present when first_introduced available |
| 189 | # --------------------------------------------------------------------------- |
| 190 | |
| 191 | @pytest.mark.asyncio |
| 192 | async def test_t305_age_label_present( |
| 193 | client: AsyncClient, |
| 194 | db_session: AsyncSession, |
| 195 | ) -> None: |
| 196 | """A sym2-td-date column must appear in every symbol row.""" |
| 197 | repo = await create_repo(db_session, owner="gabriel") |
| 198 | introduced = _dt.datetime(2024, 6, 1, tzinfo=timezone.utc) |
| 199 | await _seed_symbol( |
| 200 | db_session, repo.repo_id, "src/e.py::old_fn", |
| 201 | first_introduced=introduced, |
| 202 | ) |
| 203 | await db_session.commit() |
| 204 | |
| 205 | resp = await client.get(f"/gabriel/{repo.slug}/symbols") |
| 206 | assert resp.status_code == 200 |
| 207 | assert "sym2-td-date" in resp.text |
| 208 | |
| 209 | |
| 210 | # --------------------------------------------------------------------------- |
| 211 | # T306 — hero title has no gradient-text class |
| 212 | # --------------------------------------------------------------------------- |
| 213 | |
| 214 | @pytest.mark.asyncio |
| 215 | async def test_t306_no_gradient_text_on_hero( |
| 216 | client: AsyncClient, |
| 217 | db_session: AsyncSession, |
| 218 | ) -> None: |
| 219 | """The symbols page hero title must not use gradient-text.""" |
| 220 | repo = await create_repo(db_session, owner="gabriel") |
| 221 | await db_session.commit() |
| 222 | |
| 223 | resp = await client.get(f"/gabriel/{repo.slug}/symbols") |
| 224 | assert resp.status_code == 200 |
| 225 | |
| 226 | # The hero title h1 must not contain gradient-text |
| 227 | # Simple heuristic: the phrase appears only in non-h1 context if at all |
| 228 | import re |
| 229 | h1_blocks = re.findall(r"<h1[^>]*>.*?</h1>", resp.text, re.DOTALL) |
| 230 | for block in h1_blocks: |
| 231 | assert "gradient-text" not in block, \ |
| 232 | "gradient-text class found in h1 — hero title must use plain color" |
| 233 | |
| 234 | |
| 235 | # --------------------------------------------------------------------------- |
| 236 | # T307 — heat fill SCSS uses solid color, not linear-gradient |
| 237 | # --------------------------------------------------------------------------- |
| 238 | |
| 239 | def test_t307_heat_fill_no_gradient() -> None: |
| 240 | """The .sym2-heat-fill rule in SCSS must not use linear-gradient.""" |
| 241 | scss = SCSS_SYMBOLS.read_text() |
| 242 | |
| 243 | # Find the .sym2-heat-fill block |
| 244 | import re |
| 245 | match = re.search(r"\.sym2-heat-fill\s*\{([^}]+)\}", scss) |
| 246 | assert match is not None, ".sym2-heat-fill not found in SCSS" |
| 247 | block = match.group(1) |
| 248 | assert "linear-gradient" not in block, \ |
| 249 | ".sym2-heat-fill must use solid background color, not linear-gradient" |
| 250 | |
| 251 | |
| 252 | # --------------------------------------------------------------------------- |
| 253 | # T308 — hero ::before has no radial-gradient in SCSS |
| 254 | # --------------------------------------------------------------------------- |
| 255 | |
| 256 | def test_t308_hero_before_no_radial_gradient() -> None: |
| 257 | """The .sym2-hero ::before rule must not use radial-gradient.""" |
| 258 | scss = SCSS_SYMBOLS.read_text() |
| 259 | |
| 260 | import re |
| 261 | # Find sym2-hero block (the list-page hero, not sym2-hero--detail) |
| 262 | match = re.search(r"\.sym2-hero\b[^{]*\{(.+?)(?=\n\.sym2-hero-inner)", scss, re.DOTALL) |
| 263 | if match: |
| 264 | block = match.group(1) |
| 265 | assert "radial-gradient" not in block, \ |
| 266 | ".sym2-hero ::before must not use radial-gradient" |
File History
14 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b
Merge branch 'fix/wire-push-external-parent-manifest' into dev
Human
8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226
Merge branch 'fix/two-column-scroll-layout' into dev
Human
8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454
chore: bump version to 0.2.0.dev2 — nightly.2, matching muse
Sonnet 4.6
patch
11 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2
chore: bump version to 0.2.0rc15 for musehub#113 fix release
Sonnet 4.6
patch
14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352
Merge branch 'task/version-tags-phase3-server' into dev
Human
17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53
merge: rescue snapshot-recovery hardening (c00aa21d) into d…
Opus 4.8
minor
⚠
29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a
fix: remove false-positive proposal_comments index drop fro…
Sonnet 4.6
patch
33 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3
feat: render markdown mists as HTML with heading anchor links
Sonnet 4.6
patch
34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6
Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump …
Human
35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee
Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump …
Human
35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c
Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo…
Human
35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd
Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop…
Human
35 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f
fix: use wire_bytes not mpack_bytes_raw in compute_object_b…
Sonnet 4.6
patch
48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583
rename: delta_add → delta_upsert across wire format, models…
Sonnet 4.6
patch
50 days ago