gabriel / musehub public
test_musehub_ui_sessions_ssr.py python
241 lines 7.5 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """SSR tests for the MuseHub sessions list and session detail pages (issue #573).
2
3 Verifies that both ``GET /{owner}/{repo_slug}/sessions`` and
4 ``GET /{owner}/{repo_slug}/sessions/{session_id}`` render session
5 data server-side rather than relying on client-side JavaScript fetches.
6
7 Tests:
8 - test_sessions_list_renders_session_name_server_side
9 — Seed a session, GET page, assert session_id present in HTML without JS
10 - test_sessions_list_active_badge_present
11 — Active session → badge with "live" in HTML
12 - test_sessions_list_htmx_fragment_path
13 — GET with HX-Request: true → fragment only (no <html>)
14 - test_sessions_list_empty_state_when_no_sessions
15 — No sessions → empty state rendered server-side
16 - test_session_detail_renders_session_id
17 — GET detail page, assert session metadata in HTML
18 - test_session_detail_renders_participants
19 — Seed participant, assert user_id in HTML
20 - test_session_detail_unknown_id_404
21 — Non-existent session_id → 404
22 """
23 from __future__ import annotations
24
25 from musehub.db.musehub_models import MusehubIdentity
26
27 import uuid
28 from datetime import datetime, timezone
29
30 import pytest
31 from httpx import AsyncClient
32 from sqlalchemy.ext.asyncio import AsyncSession
33
34 from musehub.db.musehub_models import MusehubRepo, MusehubSession
35 from musehub.types.json_types import StrDict
36
37 _OWNER = "composer"
38 _SLUG = "symphony-no-9"
39 _USER_ID = "550e8400-e29b-41d4-a716-446655440000" # matches test_user fixture
40
41
42 # ---------------------------------------------------------------------------
43 # Seed helpers
44 # ---------------------------------------------------------------------------
45
46
47 async def _make_repo(db: AsyncSession) -> str:
48 """Seed a repo and return its repo_id string."""
49 repo = MusehubRepo(
50 name=_SLUG,
51 owner=_OWNER,
52 slug=_SLUG,
53 visibility="public",
54 owner_user_id=_USER_ID,
55 )
56 db.add(repo)
57 await db.commit()
58 await db.refresh(repo)
59 return str(repo.repo_id)
60
61
62 async def _make_session(
63 db: AsyncSession,
64 repo_id: str,
65 *,
66 is_active: bool = False,
67 participants: list[str] | None = None,
68 intent: str = "Record the final movement",
69 location: str = "Studio A",
70 notes: str = "",
71 commits: list[str] | None = None,
72 ) -> MusehubSession:
73 """Seed a recording session and return the ORM row."""
74 session_id = str(uuid.uuid4())
75 started_at = datetime.now(timezone.utc)
76 ended_at = None if is_active else started_at
77 row = MusehubSession(
78 session_id=session_id,
79 repo_id=repo_id,
80 started_at=started_at,
81 ended_at=ended_at,
82 participants=participants or [],
83 intent=intent,
84 location=location,
85 notes=notes,
86 commits=commits or [],
87 is_active=is_active,
88 )
89 db.add(row)
90 await db.commit()
91 await db.refresh(row)
92 return row
93
94
95 # ---------------------------------------------------------------------------
96 # Sessions list SSR tests
97 # ---------------------------------------------------------------------------
98
99
100 async def test_sessions_list_renders_session_name_server_side(
101 client: AsyncClient,
102 auth_headers: StrDict,
103 db_session: AsyncSession,
104 test_user: MusehubIdentity,
105 ) -> None:
106 """Session ID appears in the HTML response without a JS round-trip.
107
108 The handler queries the DB during the request and inlines the session
109 identifier into the Jinja2 template so browsers receive a complete page
110 on first load.
111 """
112 repo_id = await _make_repo(db_session)
113 row = await _make_session(db_session, repo_id, intent="Compose bridge section")
114 resp = await client.get(
115 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
116 )
117 assert resp.status_code == 200
118 body = resp.text
119 assert row.session_id[:8] in body
120 assert "session-row" in body
121
122
123 async def test_sessions_list_active_badge_present(
124 client: AsyncClient,
125 auth_headers: StrDict,
126 db_session: AsyncSession,
127 test_user: MusehubIdentity,
128 ) -> None:
129 """Active session renders a live badge in the server-rendered HTML."""
130 repo_id = await _make_repo(db_session)
131 await _make_session(db_session, repo_id, is_active=True)
132 resp = await client.get(
133 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
134 )
135 assert resp.status_code == 200
136 body = resp.text
137 assert "live" in body.lower() or "Live" in body
138
139
140 async def test_sessions_list_htmx_fragment_path(
141 client: AsyncClient,
142 auth_headers: StrDict,
143 db_session: AsyncSession,
144 test_user: MusehubIdentity,
145 ) -> None:
146 """GET with HX-Request: true returns rows fragment, not the full page.
147
148 When HTMX issues a partial swap request the response must NOT contain
149 the full page chrome and MUST contain the session row markup.
150 """
151 repo_id = await _make_repo(db_session)
152 row = await _make_session(db_session, repo_id)
153 htmx_headers = {**auth_headers, "HX-Request": "true"}
154 resp = await client.get(
155 f"/{_OWNER}/{_SLUG}/sessions", headers=htmx_headers
156 )
157 assert resp.status_code == 200
158 body = resp.text
159 assert row.session_id[:8] in body
160 assert "<!DOCTYPE html>" not in body
161 assert "<html" not in body
162
163
164 async def test_sessions_list_empty_state_when_no_sessions(
165 client: AsyncClient,
166 auth_headers: StrDict,
167 db_session: AsyncSession,
168 test_user: MusehubIdentity,
169 ) -> None:
170 """Empty session list renders an empty-state component server-side (no JS fetch needed)."""
171 await _make_repo(db_session)
172 resp = await client.get(
173 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
174 )
175 assert resp.status_code == 200
176 body = resp.text
177 assert '<div class="session-row' not in body
178 assert "empty-state" in body or "No sessions yet" in body
179
180
181 # ---------------------------------------------------------------------------
182 # Session detail SSR tests
183 # ---------------------------------------------------------------------------
184
185
186 async def test_session_detail_renders_session_id(
187 client: AsyncClient,
188 auth_headers: StrDict,
189 db_session: AsyncSession,
190 test_user: MusehubIdentity,
191 ) -> None:
192 """Session detail page renders the session ID and metadata server-side."""
193 repo_id = await _make_repo(db_session)
194 row = await _make_session(
195 db_session, repo_id, intent="Lay down the horn section", location="Studio B"
196 )
197 resp = await client.get(
198 f"/{_OWNER}/{_SLUG}/sessions/{row.session_id}",
199 headers=auth_headers,
200 )
201 assert resp.status_code == 200
202 body = resp.text
203 assert row.session_id[:8] in body
204 assert "Studio B" in body
205
206
207 async def test_session_detail_renders_participants(
208 client: AsyncClient,
209 auth_headers: StrDict,
210 db_session: AsyncSession,
211 test_user: MusehubIdentity,
212 ) -> None:
213 """Participant user IDs appear in the session detail HTML response."""
214 repo_id = await _make_repo(db_session)
215 row = await _make_session(
216 db_session, repo_id, participants=["alice", "bob"]
217 )
218 resp = await client.get(
219 f"/{_OWNER}/{_SLUG}/sessions/{row.session_id}",
220 headers=auth_headers,
221 )
222 assert resp.status_code == 200
223 body = resp.text
224 assert "alice" in body
225 assert "bob" in body
226
227
228 async def test_session_detail_unknown_id_404(
229 client: AsyncClient,
230 auth_headers: StrDict,
231 db_session: AsyncSession,
232 test_user: MusehubIdentity,
233 ) -> None:
234 """Non-existent session_id returns HTTP 404."""
235 await _make_repo(db_session)
236 fake_id = str(uuid.uuid4())
237 resp = await client.get(
238 f"/{_OWNER}/{_SLUG}/sessions/{fake_id}",
239 headers=auth_headers,
240 )
241 assert resp.status_code == 404
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago