gabriel / musehub public
test_phase4_gravity_detail.py python
224 lines 8.4 KB
Raw
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor ⚠ breaking 21 days ago
1 """TDD spec for Phase 4 — /intel/gravity/detail per-symbol detail page (issue #9).
2
3 New route:
4 GET /{owner}/{repo_slug}/intel/gravity/detail?address=<symbol_address>
5
6 Shows a per-symbol view with:
7 - Symbol header (name, address, kind badge, gravity_pct)
8 - Depth distribution bar chart (one bar per depth level, proportional heights)
9 - Reach numbers (direct / transitive dependents, max chain depth)
10 - Back link to /intel/gravity
11
12 New helper exposed from ui_intel:
13 _depth_bars(dist) → list of {level, count, pct} sorted by level ascending
14
15 Layers:
16 1. Helper — _depth_bars() pure function: None, empty, single, multi, sort
17 2. Route — handler registered; ?address= resolves to 200 or empty-state
18 3. Content — HTML contains name, gravity_pct, kind, reach counts
19 4. Bars — one depth bar per bucket; max bucket renders at 100%
20 5. Nav — back link present; title/breadcrumb correct
21 """
22 from __future__ import annotations
23
24 import secrets
25 from urllib.parse import quote
26
27 import pytest
28 import pytest_asyncio
29 from httpx import AsyncClient
30 from sqlalchemy.ext.asyncio import AsyncSession
31
32 from muse.core.types import fake_id
33 from musehub.db.musehub_intel_models import MusehubSymbolIntel
34 from musehub.db.musehub_repo_models import MusehubRepo
35 from tests.factories import create_repo
36
37
38 # ---------------------------------------------------------------------------
39 # Shared fixtures
40 # ---------------------------------------------------------------------------
41
42 _OWNER = "testuser"
43 _SLUG = "gravdetailrepo"
44 _ADDRESS = "musehub/storage/backends.py::S3Backend._key"
45 _DIST = {"1": 3, "2": 11, "3": 7, "4": 2}
46
47
48 @pytest_asyncio.fixture
49 async def detail_repo(db_session: AsyncSession) -> MusehubRepo:
50 return await create_repo(db_session, owner=_OWNER, slug=_SLUG)
51
52
53 @pytest_asyncio.fixture
54 async def detail_symbol(db_session: AsyncSession, detail_repo: MusehubRepo) -> MusehubSymbolIntel:
55 row = MusehubSymbolIntel(
56 repo_id=str(detail_repo.repo_id),
57 address=_ADDRESS,
58 gravity_pct=38.9,
59 gravity_direct_dependents=11,
60 gravity_transitive_dependents=733,
61 gravity_max_depth=6,
62 gravity_depth_distribution=_DIST,
63 symbol_kind="method",
64 )
65 db_session.add(row)
66 await db_session.commit()
67 return row
68
69
70 def _enc(addr: str) -> str:
71 return quote(addr, safe="")
72
73
74 # ---------------------------------------------------------------------------
75 # Layer 1 — _depth_bars() pure helper
76 # ---------------------------------------------------------------------------
77
78 class TestDepthBarsHelper:
79 def test_P4_01_none_returns_empty(self) -> None:
80 from musehub.api.routes.musehub.ui_intel import _depth_bars
81 assert _depth_bars(None) == []
82
83 def test_P4_02_empty_dict_returns_empty(self) -> None:
84 from musehub.api.routes.musehub.ui_intel import _depth_bars
85 assert _depth_bars({}) == []
86
87 def test_P4_03_single_bucket_is_100pct(self) -> None:
88 from musehub.api.routes.musehub.ui_intel import _depth_bars
89 result = _depth_bars({"3": 5})
90 assert len(result) == 1
91 assert result[0]["level"] == 3
92 assert result[0]["count"] == 5
93 assert result[0]["pct"] == 100
94
95 def test_P4_04_max_bucket_is_100_others_proportional(self) -> None:
96 from musehub.api.routes.musehub.ui_intel import _depth_bars
97 result = _depth_bars({"1": 4, "2": 8, "3": 2})
98 by_level = {r["level"]: r for r in result}
99 assert by_level[2]["pct"] == 100
100 assert by_level[1]["pct"] == 50
101 assert by_level[3]["pct"] == 25
102
103 def test_P4_05_sorted_by_level_ascending(self) -> None:
104 from musehub.api.routes.musehub.ui_intel import _depth_bars
105 result = _depth_bars({"3": 1, "1": 5, "2": 3})
106 assert [r["level"] for r in result] == [1, 2, 3]
107
108 def test_P4_06_string_keys_sorted_as_int(self) -> None:
109 from musehub.api.routes.musehub.ui_intel import _depth_bars
110 result = _depth_bars({"9": 1, "10": 2, "1": 5})
111 assert [r["level"] for r in result] == [1, 9, 10]
112
113
114 # ---------------------------------------------------------------------------
115 # Layer 2 — Route registration
116 # ---------------------------------------------------------------------------
117
118 class TestRouteRegistration:
119 def test_P4_07_detail_route_registered(self) -> None:
120 from musehub.api.routes.musehub.ui_intel import router
121 paths = [r.path for r in router.routes]
122 assert any("gravity/detail" in p for p in paths)
123
124
125 # ---------------------------------------------------------------------------
126 # Layer 3 — Route responses
127 # ---------------------------------------------------------------------------
128
129 class TestRouteResponses:
130 @pytest.mark.asyncio
131 async def test_P4_08_known_address_returns_200(
132 self, client: AsyncClient, detail_repo: MusehubRepo, detail_symbol: MusehubSymbolIntel
133 ) -> None:
134 resp = await client.get(
135 f"/{_OWNER}/{_SLUG}/intel/gravity/detail?address={_enc(_ADDRESS)}"
136 )
137 assert resp.status_code == 200
138
139 @pytest.mark.asyncio
140 async def test_P4_09_unknown_address_returns_200_with_empty_state(
141 self, client: AsyncClient, detail_repo: MusehubRepo
142 ) -> None:
143 resp = await client.get(
144 f"/{_OWNER}/{_SLUG}/intel/gravity/detail?address=no%2F%3A%3Asuch"
145 )
146 assert resp.status_code == 200
147 assert "empty" in resp.text.lower() or "no gravity" in resp.text.lower() or "no data" in resp.text.lower()
148
149 @pytest.mark.asyncio
150 async def test_P4_10_missing_address_param_returns_200_with_empty_state(
151 self, client: AsyncClient, detail_repo: MusehubRepo
152 ) -> None:
153 resp = await client.get(f"/{_OWNER}/{_SLUG}/intel/gravity/detail")
154 assert resp.status_code == 200
155 html = resp.text.lower()
156 assert "no gravity" in html or "no data" in html or "empty" in html
157
158
159 # ---------------------------------------------------------------------------
160 # Layer 4 — Template content
161 # ---------------------------------------------------------------------------
162
163 class TestTemplateContent:
164 @pytest.mark.asyncio
165 async def test_P4_11_symbol_name_in_html(
166 self, client: AsyncClient, detail_repo: MusehubRepo, detail_symbol: MusehubSymbolIntel
167 ) -> None:
168 resp = await client.get(
169 f"/{_OWNER}/{_SLUG}/intel/gravity/detail?address={_enc(_ADDRESS)}"
170 )
171 assert "S3Backend._key" in resp.text
172
173 @pytest.mark.asyncio
174 async def test_P4_12_gravity_pct_formatted_in_html(
175 self, client: AsyncClient, detail_repo: MusehubRepo, detail_symbol: MusehubSymbolIntel
176 ) -> None:
177 resp = await client.get(
178 f"/{_OWNER}/{_SLUG}/intel/gravity/detail?address={_enc(_ADDRESS)}"
179 )
180 assert "38." in resp.text
181
182 @pytest.mark.asyncio
183 async def test_P4_13_kind_badge_rendered(
184 self, client: AsyncClient, detail_repo: MusehubRepo, detail_symbol: MusehubSymbolIntel
185 ) -> None:
186 resp = await client.get(
187 f"/{_OWNER}/{_SLUG}/intel/gravity/detail?address={_enc(_ADDRESS)}"
188 )
189 assert "method" in resp.text
190
191 @pytest.mark.asyncio
192 async def test_P4_14_reach_counts_rendered(
193 self, client: AsyncClient, detail_repo: MusehubRepo, detail_symbol: MusehubSymbolIntel
194 ) -> None:
195 resp = await client.get(
196 f"/{_OWNER}/{_SLUG}/intel/gravity/detail?address={_enc(_ADDRESS)}"
197 )
198 assert "11" in resp.text # direct
199 assert "733" in resp.text # transitive
200
201 @pytest.mark.asyncio
202 async def test_P4_15_one_depth_bar_per_bucket(
203 self, client: AsyncClient, detail_repo: MusehubRepo, detail_symbol: MusehubSymbolIntel
204 ) -> None:
205 resp = await client.get(
206 f"/{_OWNER}/{_SLUG}/intel/gravity/detail?address={_enc(_ADDRESS)}"
207 )
208 # _DIST has 4 levels; expect 4 depth-bar elements
209 assert resp.text.count("depth-bar") >= 4
210
211
212 # ---------------------------------------------------------------------------
213 # Layer 5 — Navigation
214 # ---------------------------------------------------------------------------
215
216 class TestNavigation:
217 @pytest.mark.asyncio
218 async def test_P4_16_back_link_to_gravity_list(
219 self, client: AsyncClient, detail_repo: MusehubRepo, detail_symbol: MusehubSymbolIntel
220 ) -> None:
221 resp = await client.get(
222 f"/{_OWNER}/{_SLUG}/intel/gravity/detail?address={_enc(_ADDRESS)}"
223 )
224 assert f"/{_OWNER}/{_SLUG}/intel/gravity" in resp.text
File History 1 commit
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 21 days ago