gabriel / musehub public
test_symbols_v2_p3_template.py python
266 lines 9.0 KB
Raw
sha256:3c58668648c7323bb9f5c6881cfe6a3f14fc93fcb73b537d253732952a5bf8bf chore: bump version to 0.2.0rc12 Sonnet 4.6 patch 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 1 commit
sha256:3c58668648c7323bb9f5c6881cfe6a3f14fc93fcb73b537d253732952a5bf8bf chore: bump version to 0.2.0rc12 Sonnet 4.6 patch 8 days ago