gabriel / musehub public
test_musehub_ui_emotion_diff.py python
243 lines 8.8 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """Tests for the MuseHub emotion-diff UI page.
2
3 Covers:
4 - test_emotion_diff_page_renders — GET /{owner}/{repo}/emotion-diff/{base}...{head} returns 200 HTML
5 - test_emotion_diff_page_no_auth_required — accessible without JWT
6 - test_emotion_diff_page_invalid_ref_404 — refs without '...' separator return 404
7 - test_emotion_diff_page_unknown_owner_404 — unknown owner/slug returns 404
8 - test_emotion_diff_page_includes_radar — page contains server-rendered SVG radar charts
9 - test_emotion_diff_page_includes_8_dimensions — page contains all 8 emotion-diff axis labels (SSR)
10 - test_emotion_diff_page_includes_delta_chart — page contains per-axis delta table (SSR)
11 - test_emotion_diff_page_includes_trajectory — page contains "Emotional Trajectory" section
12 - test_emotion_diff_page_includes_listen_button — page contains "Listen" comparison buttons
13 - test_emotion_diff_page_includes_interpretation — page contains interpretation text
14 - test_emotion_diff_json_response — ?format=json returns EmotionDiffResponse shape
15 - test_emotion_diff_page_empty_base_ref_404 — base ref empty returns 404
16 - test_emotion_diff_page_empty_head_ref_404 — head ref empty returns 404
17 """
18 from __future__ import annotations
19
20 import pytest
21 from httpx import AsyncClient
22 from sqlalchemy.ext.asyncio import AsyncSession
23
24 from musehub.db.musehub_models import MusehubRepo
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31
32 async def _make_repo(db_session: AsyncSession) -> str:
33 """Seed a minimal repo and return its repo_id."""
34 repo = MusehubRepo(
35 name="test-beats",
36 owner="testuser",
37 slug="test-beats",
38 visibility="private",
39 owner_user_id="test-owner",
40 )
41 db_session.add(repo)
42 await db_session.commit()
43 await db_session.refresh(repo)
44 return str(repo.repo_id)
45
46
47 _BASE_URL = "/testuser/test-beats/emotion-diff/main...feature"
48
49
50 # ---------------------------------------------------------------------------
51 # Issue #432 — emotion-diff UI page
52 # ---------------------------------------------------------------------------
53
54
55 @pytest.mark.anyio
56 async def test_emotion_diff_page_renders(
57 client: AsyncClient,
58 db_session: AsyncSession,
59 ) -> None:
60 """GET /{owner}/{slug}/emotion-diff/{base}...{head} returns 200 HTML."""
61 await _make_repo(db_session)
62 response = await client.get(_BASE_URL)
63 assert response.status_code == 200
64 assert "text/html" in response.headers["content-type"]
65 body = response.text
66 assert "MuseHub" in body
67 assert "main" in body
68 assert "feature" in body
69
70
71 @pytest.mark.anyio
72 async def test_emotion_diff_page_no_auth_required(
73 client: AsyncClient,
74 db_session: AsyncSession,
75 ) -> None:
76 """Emotion-diff page is accessible without a JWT token."""
77 await _make_repo(db_session)
78 response = await client.get(_BASE_URL)
79 assert response.status_code == 200
80
81
82 @pytest.mark.anyio
83 async def test_emotion_diff_page_invalid_ref_404(
84 client: AsyncClient,
85 db_session: AsyncSession,
86 ) -> None:
87 """Emotion-diff path without '...' separator returns 404."""
88 await _make_repo(db_session)
89 response = await client.get("/testuser/test-beats/emotion-diff/mainfeature")
90 assert response.status_code == 404
91
92
93 @pytest.mark.anyio
94 async def test_emotion_diff_page_unknown_owner_404(
95 client: AsyncClient,
96 ) -> None:
97 """Unknown owner/slug combination returns 404 on emotion-diff page."""
98 response = await client.get("/nobody/norepo/emotion-diff/main...feature")
99 assert response.status_code == 404
100
101
102 @pytest.mark.anyio
103 async def test_emotion_diff_page_empty_base_ref_404(
104 client: AsyncClient,
105 db_session: AsyncSession,
106 ) -> None:
107 """Emotion-diff path with empty base ref (starts with '...') returns 404."""
108 await _make_repo(db_session)
109 response = await client.get("/testuser/test-beats/emotion-diff/...feature")
110 assert response.status_code == 404
111
112
113 @pytest.mark.anyio
114 async def test_emotion_diff_page_empty_head_ref_404(
115 client: AsyncClient,
116 db_session: AsyncSession,
117 ) -> None:
118 """Emotion-diff path with empty head ref (ends with '...') returns 404."""
119 await _make_repo(db_session)
120 response = await client.get("/testuser/test-beats/emotion-diff/main...")
121 assert response.status_code == 404
122
123
124 @pytest.mark.anyio
125 async def test_emotion_diff_page_includes_radar(
126 client: AsyncClient,
127 db_session: AsyncSession,
128 ) -> None:
129 """Emotion-diff page HTML contains server-rendered SVG radar charts for both refs."""
130 await _make_repo(db_session)
131 response = await client.get(_BASE_URL)
132 assert response.status_code == 200
133 body = response.text
134 assert "<svg" in body
135 assert "8-Dimension Emotional Signature" in body
136
137
138 @pytest.mark.anyio
139 async def test_emotion_diff_page_includes_8_dimensions(
140 client: AsyncClient,
141 db_session: AsyncSession,
142 ) -> None:
143 """Emotion-diff page HTML renders all 8 emotional dimension axis labels (SSR)."""
144 await _make_repo(db_session)
145 response = await client.get(_BASE_URL)
146 assert response.status_code == 200
147 body = response.text
148 # All 8 axis labels from EmotionVector8D must appear in the SSR content
149 for label in ("Valence", "Energy", "Tension", "Complexity", "Warmth", "Brightness", "Darkness", "Playfulness"):
150 assert label in body, f"Axis label '{label}' missing from SSR page"
151
152
153 @pytest.mark.anyio
154 async def test_emotion_diff_page_includes_delta_chart(
155 client: AsyncClient,
156 db_session: AsyncSession,
157 ) -> None:
158 """Emotion-diff page HTML contains the per-axis delta table (SSR)."""
159 await _make_repo(db_session)
160 response = await client.get(_BASE_URL)
161 assert response.status_code == 200
162 body = response.text
163 assert "Per-Axis Delta" in body
164
165
166 @pytest.mark.anyio
167 async def test_emotion_diff_page_includes_trajectory(
168 client: AsyncClient,
169 db_session: AsyncSession,
170 ) -> None:
171 """Emotion-diff page HTML contains the 'Emotional Trajectory' section heading."""
172 await _make_repo(db_session)
173 response = await client.get(_BASE_URL)
174 assert response.status_code == 200
175 body = response.text
176 # The trajectory section heading is still in the SSR template
177 # (server-side CSS bars replace JS sparklines)
178 assert "Emotional" in body
179
180
181 @pytest.mark.anyio
182 async def test_emotion_diff_page_includes_listen_button(
183 client: AsyncClient,
184 db_session: AsyncSession,
185 ) -> None:
186 """Emotion-diff page HTML contains listen comparison buttons for both refs."""
187 await _make_repo(db_session)
188 response = await client.get(_BASE_URL)
189 assert response.status_code == 200
190 body = response.text
191 assert "Listen Base" in body
192 assert "Listen Head" in body
193 assert "listen" in body
194
195
196 @pytest.mark.anyio
197 async def test_emotion_diff_page_includes_interpretation(
198 client: AsyncClient,
199 db_session: AsyncSession,
200 ) -> None:
201 """Emotion-diff page HTML contains interpretation output from the emotion-diff service."""
202 await _make_repo(db_session)
203 response = await client.get(_BASE_URL)
204 assert response.status_code == 200
205 body = response.text
206 # The SSR template renders the interpretation string directly — it always
207 # starts with "This commit" per compute_emotion_diff().
208 assert "This commit" in body or "emotional" in body.lower()
209
210
211 @pytest.mark.anyio
212 async def test_emotion_diff_json_response(
213 client: AsyncClient,
214 db_session: AsyncSession,
215 ) -> None:
216 """GET /{owner}/{slug}/emotion-diff/{refs}?format=json returns EmotionDiffResponse."""
217 await _make_repo(db_session)
218 response = await client.get(f"{_BASE_URL}?format=json")
219 assert response.status_code == 200
220 assert "application/json" in response.headers["content-type"]
221 body = response.json()
222 # EmotionDiffResponse camelCase fields
223 assert "baseRef" in body
224 assert "headRef" in body
225 assert "baseEmotion" in body
226 assert "headEmotion" in body
227 assert "delta" in body
228 assert "interpretation" in body
229 assert "repoId" in body
230 # All 8 axes on base and head emotion vectors
231 for vec_key in ("baseEmotion", "headEmotion"):
232 vec = body[vec_key]
233 for axis in ("valence", "energy", "tension", "complexity", "warmth", "brightness", "darkness", "playfulness"):
234 assert axis in vec, f"Axis '{axis}' missing from {vec_key}"
235 assert 0.0 <= vec[axis] <= 1.0, f"{vec_key}.{axis} out of [0, 1]"
236 # Delta axes allow signed values in [-1, 1]
237 delta = body["delta"]
238 for axis in ("valence", "energy", "tension", "complexity", "warmth", "brightness", "darkness", "playfulness"):
239 assert axis in delta, f"Axis '{axis}' missing from delta"
240 assert -1.0 <= delta[axis] <= 1.0, f"delta.{axis} out of [-1, 1]"
241 # interpretation is a non-empty string
242 assert isinstance(body["interpretation"], str)
243 assert len(body["interpretation"]) > 10