gabriel / musehub public
test_phase2_dead_route.py python
183 lines 6.1 KB
Raw
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor ⚠ breaking 20 days ago
1 """TDD spec for Phase 2 — /intel/dead route registration + empty state (issue #10).
2
3 New route:
4 GET /{owner}/{repo_slug}/intel/dead
5
6 Query params:
7 confidence (optional) — filter to one tier: high | medium | low
8 show_dismissed (optional, default false) — include dismissed rows
9
10 Handler queries musehub_intel_dead ordered by:
11 confidence tier (high → medium → low), then address ascending.
12
13 Template: musehub/templates/musehub/pages/intel_dead.html
14
15 Layers:
16 1. Route — "intel/dead" registered in ui_intel router
17 2. Basic — GET /{owner}/{repo_slug}/intel/dead → 200
18 3. Not found — unknown repo → 404
19 4. Empty — zero dead rows → 200 with empty-state text
20 5. Filter — ?confidence=high filters to high tier only
21 6. Dismissed — ?show_dismissed=true includes dismissed rows
22 7. MIME — response Content-Type is text/html
23 """
24 from __future__ import annotations
25
26 import secrets
27
28 import pytest
29 import pytest_asyncio
30 from httpx import AsyncClient
31 from sqlalchemy.dialects.postgresql import insert as pg_insert
32 from sqlalchemy.ext.asyncio import AsyncSession
33
34 from muse.core.types import fake_id, long_id
35 from musehub.db.musehub_intel_models import MusehubIntelDead
36 from tests.factories import create_repo
37
38
39 def _uid() -> str:
40 return fake_id(secrets.token_hex(16))
41
42
43 _OWNER = "testuser"
44 _SLUG = "deadrouterepo"
45
46
47 async def _seed_dead(
48 session: AsyncSession,
49 repo_id: str,
50 *,
51 address: str,
52 confidence: str = "high",
53 dismissed: bool = False,
54 ) -> None:
55 stmt = (
56 pg_insert(MusehubIntelDead)
57 .values(
58 repo_id=repo_id,
59 address=address,
60 kind="function",
61 confidence=confidence,
62 reason="test reason",
63 ref=long_id("a" * 64),
64 dismissed=dismissed,
65 )
66 .on_conflict_do_nothing()
67 )
68 await session.execute(stmt)
69 await session.flush()
70
71
72 # ---------------------------------------------------------------------------
73 # Fixtures
74 # ---------------------------------------------------------------------------
75
76 @pytest_asyncio.fixture
77 async def dead_repo(db_session: AsyncSession) -> MusehubRepo:
78 return await create_repo(db_session, owner=_OWNER, slug=_SLUG)
79
80
81 # ---------------------------------------------------------------------------
82 # Layer 1 — Route registration
83 # ---------------------------------------------------------------------------
84
85 class TestDeadRouteRegistration:
86
87 def test_P2_01_dead_route_registered(self) -> None:
88 from musehub.api.routes.musehub.ui_intel import router
89 paths = [r.path for r in router.routes]
90 assert any("intel/dead" in p for p in paths)
91
92
93 # ---------------------------------------------------------------------------
94 # Layer 2 — Basic response
95 # ---------------------------------------------------------------------------
96
97 class TestDeadRouteBasic:
98
99 @pytest.mark.asyncio
100 async def test_P2_02_get_dead_returns_200(
101 self, client: AsyncClient, dead_repo: MusehubRepo
102 ) -> None:
103 resp = await client.get(f"/{_OWNER}/{_SLUG}/intel/dead")
104 assert resp.status_code == 200
105
106 @pytest.mark.asyncio
107 async def test_P2_03_unknown_repo_returns_404(
108 self, client: AsyncClient
109 ) -> None:
110 resp = await client.get("/nobody/nonexistentrepo123/intel/dead")
111 assert resp.status_code == 404
112
113
114 # ---------------------------------------------------------------------------
115 # Layer 3 — Empty state
116 # ---------------------------------------------------------------------------
117
118 class TestDeadRouteEmptyState:
119
120 @pytest.mark.asyncio
121 async def test_P2_04_zero_rows_empty_state_text(
122 self, client: AsyncClient, dead_repo: MusehubRepo
123 ) -> None:
124 resp = await client.get(f"/{_OWNER}/{_SLUG}/intel/dead")
125 assert resp.status_code == 200
126 html = resp.text.lower()
127 assert "no dead code" in html or "clean" in html or "no candidates" in html
128
129
130 # ---------------------------------------------------------------------------
131 # Layer 4 — Confidence filter
132 # ---------------------------------------------------------------------------
133
134 class TestDeadRouteFilter:
135
136 @pytest.mark.asyncio
137 async def test_P2_05_confidence_high_filters_to_high_only(
138 self, client: AsyncClient, db_session: AsyncSession, dead_repo: MusehubRepo
139 ) -> None:
140 await db_session.commit()
141 await _seed_dead(db_session, dead_repo.repo_id, address="pkg/a.py::high_fn", confidence="high")
142 await _seed_dead(db_session, dead_repo.repo_id, address="pkg/b.py::low_fn", confidence="low")
143 await db_session.commit()
144
145 resp = await client.get(f"/{_OWNER}/{_SLUG}/intel/dead?confidence=high")
146 assert resp.status_code == 200
147 assert "high_fn" in resp.text
148 assert "low_fn" not in resp.text
149
150
151 # ---------------------------------------------------------------------------
152 # Layer 5 — Dismissed visibility
153 # ---------------------------------------------------------------------------
154
155 class TestDeadRouteDismissed:
156
157 @pytest.mark.asyncio
158 async def test_P2_06_show_dismissed_includes_dismissed_rows(
159 self, client: AsyncClient, db_session: AsyncSession, dead_repo: MusehubRepo
160 ) -> None:
161 await db_session.commit()
162 await _seed_dead(db_session, dead_repo.repo_id, address="pkg/c.py::dismissed_fn", dismissed=True)
163 await db_session.commit()
164
165 resp_default = await client.get(f"/{_OWNER}/{_SLUG}/intel/dead")
166 assert "dismissed_fn" not in resp_default.text
167
168 resp_show = await client.get(f"/{_OWNER}/{_SLUG}/intel/dead?show_dismissed=true")
169 assert "dismissed_fn" in resp_show.text
170
171
172 # ---------------------------------------------------------------------------
173 # Layer 6 — MIME type
174 # ---------------------------------------------------------------------------
175
176 class TestDeadRouteMime:
177
178 @pytest.mark.asyncio
179 async def test_P2_07_content_type_is_html(
180 self, client: AsyncClient, dead_repo: MusehubRepo
181 ) -> None:
182 resp = await client.get(f"/{_OWNER}/{_SLUG}/intel/dead")
183 assert "text/html" in resp.headers.get("content-type", "")
File History 1 commit
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 20 days ago