gabriel / musehub public
test_musehub_ui_credits_activity_ssr.py python
247 lines 7.5 KB
e6fad116 Remove all Stori, Maestro, and AgentCeption references; rebrand to Muse VCS Gabriel Cardona <gabriel@tellurstori.com> 6d ago
1 """SSR tests for the Muse Hub credits and activity pages (issue #574).
2
3 Verifies that ``GET /musehub/ui/{owner}/{repo_slug}/credits`` and
4 ``GET /musehub/ui/{owner}/{repo_slug}/activity`` render data server-side
5 rather than relying on client-side JavaScript fetches.
6
7 Tests:
8 - test_credits_page_renders_contributor_name_server_side
9 — Seed a commit, GET credits page, assert author name in HTML
10 - test_credits_page_shows_total_contributors
11 — Total contributor count is present in SSR HTML
12 - test_activity_page_renders_event_server_side
13 — Seed an event, GET activity page, assert event description in HTML
14 - test_activity_page_filter_form_has_hx_get
15 — Filter form has hx-get attribute for HTMX partial updates
16 - test_activity_page_htmx_fragment_path
17 — HX-Request: true returns fragment only (no <html>)
18 - test_activity_page_event_type_filter
19 — ?event_type=commit_pushed returns only matching events
20 """
21 from __future__ import annotations
22
23 import uuid
24 from datetime import datetime, timezone
25
26 import pytest
27 from httpx import AsyncClient
28 from sqlalchemy.ext.asyncio import AsyncSession
29
30 from musehub.db.musehub_models import MusehubCommit, MusehubEvent, MusehubRepo
31
32 _OWNER = "muse-artist"
33 _SLUG = "debut-album"
34 _USER_ID = "550e8400-e29b-41d4-a716-446655440000" # matches test_user fixture
35
36
37 # ---------------------------------------------------------------------------
38 # Seed helpers
39 # ---------------------------------------------------------------------------
40
41
42 async def _make_repo(db: AsyncSession) -> str:
43 """Seed a repo and return its repo_id string."""
44 repo = MusehubRepo(
45 name=_SLUG,
46 owner=_OWNER,
47 slug=_SLUG,
48 visibility="public",
49 owner_user_id=_USER_ID,
50 )
51 db.add(repo)
52 await db.commit()
53 await db.refresh(repo)
54 return str(repo.repo_id)
55
56
57 async def _make_commit(
58 db: AsyncSession,
59 repo_id: str,
60 *,
61 author: str = "alice",
62 message: str = "Add melody",
63 ) -> MusehubCommit:
64 """Seed a commit and return the ORM object."""
65 commit = MusehubCommit(
66 commit_id=str(uuid.uuid4())[:16],
67 repo_id=repo_id,
68 branch="main",
69 parent_ids=[],
70 message=message,
71 author=author,
72 timestamp=datetime.now(tz=timezone.utc),
73 )
74 db.add(commit)
75 await db.commit()
76 await db.refresh(commit)
77 return commit
78
79
80 async def _make_event(
81 db: AsyncSession,
82 repo_id: str,
83 *,
84 event_type: str = "commit_pushed",
85 actor: str = "alice",
86 description: str = "Pushed a commit",
87 ) -> MusehubEvent:
88 """Seed an activity event and return the ORM object."""
89 event = MusehubEvent(
90 event_id=str(uuid.uuid4()),
91 repo_id=repo_id,
92 event_type=event_type,
93 actor=actor,
94 description=description,
95 event_metadata={},
96 created_at=datetime.now(tz=timezone.utc),
97 )
98 db.add(event)
99 await db.commit()
100 await db.refresh(event)
101 return event
102
103
104 # ---------------------------------------------------------------------------
105 # Credits page SSR tests
106 # ---------------------------------------------------------------------------
107
108
109 @pytest.mark.anyio
110 async def test_credits_page_renders_contributor_name_server_side(
111 client: AsyncClient,
112 db_session: AsyncSession,
113 test_user: object,
114 ) -> None:
115 """Author name appears in the credits HTML response without a JS round-trip.
116
117 The handler aggregates contributor credits from commit history server-side
118 and inlines them into the Jinja2 template.
119 """
120 repo_id = await _make_repo(db_session)
121 await _make_commit(db_session, repo_id, author="charlie-contributor")
122
123 resp = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/credits")
124 assert resp.status_code == 200
125 assert "text/html" in resp.headers["content-type"]
126 assert "charlie-contributor" in resp.text
127
128
129 @pytest.mark.anyio
130 async def test_credits_page_shows_total_contributors(
131 client: AsyncClient,
132 db_session: AsyncSession,
133 test_user: object,
134 ) -> None:
135 """Total contributor count is rendered server-side in the credits HTML."""
136 repo_id = await _make_repo(db_session)
137 await _make_commit(db_session, repo_id, author="alice")
138 await _make_commit(db_session, repo_id, author="bob")
139
140 resp = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/credits")
141 assert resp.status_code == 200
142 # The template renders e.g. "2 contributors"
143 assert "2 contributor" in resp.text
144
145
146 # ---------------------------------------------------------------------------
147 # Activity page SSR tests
148 # ---------------------------------------------------------------------------
149
150
151 @pytest.mark.anyio
152 async def test_activity_page_renders_event_server_side(
153 client: AsyncClient,
154 db_session: AsyncSession,
155 test_user: object,
156 ) -> None:
157 """Event description appears in the activity HTML response without a JS round-trip.
158
159 The handler fetches events from the DB server-side and inlines them
160 into the Jinja2 template.
161 """
162 repo_id = await _make_repo(db_session)
163 await _make_event(
164 db_session, repo_id,
165 event_type="commit_pushed",
166 actor="dana",
167 description="Pushed feat/synth-bass",
168 )
169
170 resp = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/activity")
171 assert resp.status_code == 200
172 assert "text/html" in resp.headers["content-type"]
173 assert "Pushed feat/synth-bass" in resp.text
174
175
176 @pytest.mark.anyio
177 async def test_activity_page_filter_form_has_hx_get(
178 client: AsyncClient,
179 db_session: AsyncSession,
180 test_user: object,
181 ) -> None:
182 """The event-type filter form has hx-get for HTMX partial updates."""
183 await _make_repo(db_session)
184
185 resp = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/activity")
186 assert resp.status_code == 200
187 assert "hx-get" in resp.text
188
189
190 @pytest.mark.anyio
191 async def test_activity_page_htmx_fragment_path(
192 client: AsyncClient,
193 db_session: AsyncSession,
194 test_user: object,
195 ) -> None:
196 """GET with HX-Request: true returns the rows fragment, not the full page.
197
198 When HTMX issues a partial swap request it sends HX-Request: true. The
199 response must NOT contain full-page chrome and MUST contain the event rows.
200 """
201 repo_id = await _make_repo(db_session)
202 await _make_event(
203 db_session, repo_id,
204 event_type="pr_opened",
205 actor="eve",
206 description="Opened PR: add chorus",
207 )
208
209 resp = await client.get(
210 f"/musehub/ui/{_OWNER}/{_SLUG}/activity",
211 headers={"HX-Request": "true"},
212 )
213 assert resp.status_code == 200
214 assert "text/html" in resp.headers["content-type"]
215 assert "<html" not in resp.text
216 assert "<!DOCTYPE html>" not in resp.text
217 assert "Opened PR: add chorus" in resp.text
218
219
220 @pytest.mark.anyio
221 async def test_activity_page_event_type_filter(
222 client: AsyncClient,
223 db_session: AsyncSession,
224 test_user: object,
225 ) -> None:
226 """?event_type=commit_pushed renders only commit_pushed events in the HTML."""
227 repo_id = await _make_repo(db_session)
228 await _make_event(
229 db_session, repo_id,
230 event_type="commit_pushed",
231 actor="frank",
232 description="Pushed commit: add bassline",
233 )
234 await _make_event(
235 db_session, repo_id,
236 event_type="pr_merged",
237 actor="grace",
238 description="Merged PR: horn section",
239 )
240
241 resp = await client.get(
242 f"/musehub/ui/{_OWNER}/{_SLUG}/activity",
243 params={"event_type": "commit_pushed"},
244 )
245 assert resp.status_code == 200
246 assert "Pushed commit: add bassline" in resp.text
247 assert "Merged PR: horn section" not in resp.text