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