gabriel / musehub public
test_symbols_v2_p3_template.py python
265 lines 9.0 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 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 import musehub_models as db
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 = db.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 = db.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, kind=None, 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-meta 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-meta" 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 coupling chip must appear in the row meta."""
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-coupling-chip" in resp.text
162
163
164 # ---------------------------------------------------------------------------
165 # T304 — coupling chip absent when coupling_count == 0
166 # ---------------------------------------------------------------------------
167
168 @pytest.mark.asyncio
169 async def test_t304_no_coupling_chip_when_isolated(
170 client: AsyncClient,
171 db_session: AsyncSession,
172 ) -> None:
173 """When coupling_count == 0, no coupling chip must appear."""
174 repo = await create_repo(db_session, owner="gabriel")
175 await _seed_symbol(
176 db_session, repo.repo_id, "src/d.py::solo_fn",
177 coupling_count=0,
178 )
179 await db_session.commit()
180
181 resp = await client.get(f"/gabriel/{repo.slug}/symbols")
182 assert resp.status_code == 200
183 assert "sym2-coupling-chip" not in resp.text
184
185
186 # ---------------------------------------------------------------------------
187 # T305 — age label present when first_introduced available
188 # ---------------------------------------------------------------------------
189
190 @pytest.mark.asyncio
191 async def test_t305_age_label_present(
192 client: AsyncClient,
193 db_session: AsyncSession,
194 ) -> None:
195 """A sym2-row-age element must appear for symbols with first_introduced."""
196 repo = await create_repo(db_session, owner="gabriel")
197 introduced = _dt.datetime(2024, 6, 1, tzinfo=timezone.utc)
198 await _seed_symbol(
199 db_session, repo.repo_id, "src/e.py::old_fn",
200 first_introduced=introduced,
201 )
202 await db_session.commit()
203
204 resp = await client.get(f"/gabriel/{repo.slug}/symbols")
205 assert resp.status_code == 200
206 assert "sym2-row-age" in resp.text
207
208
209 # ---------------------------------------------------------------------------
210 # T306 — hero title has no gradient-text class
211 # ---------------------------------------------------------------------------
212
213 @pytest.mark.asyncio
214 async def test_t306_no_gradient_text_on_hero(
215 client: AsyncClient,
216 db_session: AsyncSession,
217 ) -> None:
218 """The symbols page hero title must not use gradient-text."""
219 repo = await create_repo(db_session, owner="gabriel")
220 await db_session.commit()
221
222 resp = await client.get(f"/gabriel/{repo.slug}/symbols")
223 assert resp.status_code == 200
224
225 # The hero title h1 must not contain gradient-text
226 # Simple heuristic: the phrase appears only in non-h1 context if at all
227 import re
228 h1_blocks = re.findall(r"<h1[^>]*>.*?</h1>", resp.text, re.DOTALL)
229 for block in h1_blocks:
230 assert "gradient-text" not in block, \
231 "gradient-text class found in h1 — hero title must use plain color"
232
233
234 # ---------------------------------------------------------------------------
235 # T307 — heat fill SCSS uses solid color, not linear-gradient
236 # ---------------------------------------------------------------------------
237
238 def test_t307_heat_fill_no_gradient() -> None:
239 """The .sym2-heat-fill rule in SCSS must not use linear-gradient."""
240 scss = SCSS_SYMBOLS.read_text()
241
242 # Find the .sym2-heat-fill block
243 import re
244 match = re.search(r"\.sym2-heat-fill\s*\{([^}]+)\}", scss)
245 assert match is not None, ".sym2-heat-fill not found in SCSS"
246 block = match.group(1)
247 assert "linear-gradient" not in block, \
248 ".sym2-heat-fill must use solid background color, not linear-gradient"
249
250
251 # ---------------------------------------------------------------------------
252 # T308 — hero ::before has no radial-gradient in SCSS
253 # ---------------------------------------------------------------------------
254
255 def test_t308_hero_before_no_radial_gradient() -> None:
256 """The .sym2-hero ::before rule must not use radial-gradient."""
257 scss = SCSS_SYMBOLS.read_text()
258
259 import re
260 # Find sym2-hero block (the list-page hero, not sym2-hero--detail)
261 match = re.search(r"\.sym2-hero\b[^{]*\{(.+?)(?=\n\.sym2-hero-inner)", scss, re.DOTALL)
262 if match:
263 block = match.group(1)
264 assert "radial-gradient" not in block, \
265 ".sym2-hero ::before must not use radial-gradient"
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago