gabriel / musehub public
test_musehub_ui_sessions_ssr.py python
249 lines 8.1 KB
Raw
sha256:dc28fb2384d12a52d4b4fea7743873940b89d9d08ce298f96d0fdc8d694724d4 test+types: green the musehub suite and ratchet typing audi… Opus 4.8 minor ⚠ breaking 11 hours 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_identity_models import MusehubIdentity
26
27 import secrets
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.core.genesis import compute_identity_id, compute_repo_id, compute_session_id
35 from musehub.db.musehub_repo_models import MusehubRepo, MusehubSession
36 from musehub.types.json_types import StrDict
37
38 # Owner must match the auth_headers identity ("testuser") — the sessions page is
39 # owner-only (claims.handle == owner). _USER_ID is already testuser's id, so this
40 # aligns the owner string + owner_user_id + the authenticated caller.
41 _OWNER = "testuser"
42 _SLUG = "symphony-no-9"
43 _USER_ID = compute_identity_id(b"testuser")
44
45
46 # ---------------------------------------------------------------------------
47 # Seed helpers
48 # ---------------------------------------------------------------------------
49
50
51 async def _make_repo(db: AsyncSession) -> str:
52 """Seed a repo and return its repo_id string."""
53 created_at = datetime.now(tz=timezone.utc)
54 repo = MusehubRepo(
55 repo_id=compute_repo_id(_USER_ID, _SLUG, "code", created_at.isoformat()),
56 name=_SLUG,
57 owner=_OWNER,
58 slug=_SLUG,
59 visibility="public",
60 owner_user_id=_USER_ID,
61 created_at=created_at,
62 updated_at=created_at,
63 )
64 db.add(repo)
65 await db.commit()
66 await db.refresh(repo)
67 return str(repo.repo_id)
68
69
70 async def _make_session(
71 db: AsyncSession,
72 repo_id: str,
73 *,
74 is_active: bool = False,
75 participants: list[str] | None = None,
76 intent: str = "Record the final movement",
77 location: str = "Studio A",
78 notes: str = "",
79 commits: list[str] | None = None,
80 ) -> MusehubSession:
81 """Seed a recording session and return the ORM row."""
82 started_at = datetime.now(timezone.utc)
83 session_id = compute_session_id(repo_id, compute_identity_id(b"composer"), started_at.isoformat())
84 ended_at = None if is_active else started_at
85 row = MusehubSession(
86 session_id=session_id,
87 repo_id=repo_id,
88 started_at=started_at,
89 ended_at=ended_at,
90 participants=participants or [],
91 intent=intent,
92 location=location,
93 notes=notes,
94 commits=commits or [],
95 is_active=is_active,
96 )
97 db.add(row)
98 await db.commit()
99 await db.refresh(row)
100 return row
101
102
103 # ---------------------------------------------------------------------------
104 # Sessions list SSR tests
105 # ---------------------------------------------------------------------------
106
107
108 async def test_sessions_list_renders_session_name_server_side(
109 client: AsyncClient,
110 auth_headers: StrDict,
111 db_session: AsyncSession,
112 test_user: MusehubIdentity,
113 ) -> None:
114 """Session ID appears in the HTML response without a JS round-trip.
115
116 The handler queries the DB during the request and inlines the session
117 identifier into the Jinja2 template so browsers receive a complete page
118 on first load.
119 """
120 repo_id = await _make_repo(db_session)
121 row = await _make_session(db_session, repo_id, intent="Compose bridge section")
122 resp = await client.get(
123 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
124 )
125 assert resp.status_code == 200
126 body = resp.text
127 assert row.session_id[:8] in body
128 assert "session-row" in body
129
130
131 async def test_sessions_list_active_badge_present(
132 client: AsyncClient,
133 auth_headers: StrDict,
134 db_session: AsyncSession,
135 test_user: MusehubIdentity,
136 ) -> None:
137 """Active session renders a live badge in the server-rendered HTML."""
138 repo_id = await _make_repo(db_session)
139 await _make_session(db_session, repo_id, is_active=True)
140 resp = await client.get(
141 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
142 )
143 assert resp.status_code == 200
144 body = resp.text
145 assert "live" in body.lower() or "Live" in body
146
147
148 async def test_sessions_list_htmx_fragment_path(
149 client: AsyncClient,
150 auth_headers: StrDict,
151 db_session: AsyncSession,
152 test_user: MusehubIdentity,
153 ) -> None:
154 """GET with HX-Request: true returns rows fragment, not the full page.
155
156 When HTMX issues a partial swap request the response must NOT contain
157 the full page chrome and MUST contain the session row markup.
158 """
159 repo_id = await _make_repo(db_session)
160 row = await _make_session(db_session, repo_id)
161 htmx_headers = {**auth_headers, "HX-Request": "true"}
162 resp = await client.get(
163 f"/{_OWNER}/{_SLUG}/sessions", headers=htmx_headers
164 )
165 assert resp.status_code == 200
166 body = resp.text
167 assert row.session_id[:8] in body
168 assert "<!DOCTYPE html>" not in body
169 assert "<html" not in body
170
171
172 async def test_sessions_list_empty_state_when_no_sessions(
173 client: AsyncClient,
174 auth_headers: StrDict,
175 db_session: AsyncSession,
176 test_user: MusehubIdentity,
177 ) -> None:
178 """Empty session list renders an empty-state component server-side (no JS fetch needed)."""
179 await _make_repo(db_session)
180 resp = await client.get(
181 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
182 )
183 assert resp.status_code == 200
184 body = resp.text
185 assert '<div class="session-row' not in body
186 assert "empty-state" in body or "No sessions yet" in body
187
188
189 # ---------------------------------------------------------------------------
190 # Session detail SSR tests
191 # ---------------------------------------------------------------------------
192
193
194 async def test_session_detail_renders_session_id(
195 client: AsyncClient,
196 auth_headers: StrDict,
197 db_session: AsyncSession,
198 test_user: MusehubIdentity,
199 ) -> None:
200 """Session detail page renders the session ID and metadata server-side."""
201 repo_id = await _make_repo(db_session)
202 row = await _make_session(
203 db_session, repo_id, intent="Lay down the horn section", location="Studio B"
204 )
205 resp = await client.get(
206 f"/{_OWNER}/{_SLUG}/sessions/{row.session_id}",
207 headers=auth_headers,
208 )
209 assert resp.status_code == 200
210 body = resp.text
211 assert row.session_id[:8] in body
212 assert "Studio B" in body
213
214
215 async def test_session_detail_renders_participants(
216 client: AsyncClient,
217 auth_headers: StrDict,
218 db_session: AsyncSession,
219 test_user: MusehubIdentity,
220 ) -> None:
221 """Participant user IDs appear in the session detail HTML response."""
222 repo_id = await _make_repo(db_session)
223 row = await _make_session(
224 db_session, repo_id, participants=["alice", "bob"]
225 )
226 resp = await client.get(
227 f"/{_OWNER}/{_SLUG}/sessions/{row.session_id}",
228 headers=auth_headers,
229 )
230 assert resp.status_code == 200
231 body = resp.text
232 assert "alice" in body
233 assert "bob" in body
234
235
236 async def test_session_detail_unknown_id_404(
237 client: AsyncClient,
238 auth_headers: StrDict,
239 db_session: AsyncSession,
240 test_user: MusehubIdentity,
241 ) -> None:
242 """Non-existent session_id returns HTTP 404."""
243 await _make_repo(db_session)
244 fake_id = secrets.token_hex(16)
245 resp = await client.get(
246 f"/{_OWNER}/{_SLUG}/sessions/{fake_id}",
247 headers=auth_headers,
248 )
249 assert resp.status_code == 404
File History 1 commit
sha256:dc28fb2384d12a52d4b4fea7743873940b89d9d08ce298f96d0fdc8d694724d4 test+types: green the musehub suite and ratchet typing audi… Opus 4.8 minor 11 hours ago