gabriel / musehub public
test_musehub_ui_proposal_ssr.py python
212 lines 7.9 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 """SSR tests for MuseHub proposal list + proposal detail pages — issue #569.
2
3 Validates that proposal data is rendered server-side into HTML (not deferred to client
4 JS) and that HTMX fragment requests return bare HTML without the full page shell.
5
6 Covers GET /{owner}/{repo_slug}/proposals:
7 - test_proposal_list_renders_title_server_side — proposal title appears in HTML
8 - test_proposal_list_open_closed_counts_in_tabs — tab counts reflect seeded proposals
9 - test_proposal_list_htmx_fragment_on_tab_switch — HX-Request: true → fragment
10
11 Covers GET /{owner}/{repo_slug}/proposals/{proposal_id}:
12 - test_proposal_detail_renders_title_server_side — proposal title in HTML server-side
13 - test_proposal_detail_renders_diff_stats — branch info in HTML
14 - test_proposal_detail_shows_cli_hint — CLI hint replaces write-capable form
15 - test_proposal_detail_unknown_number_404 — non-existent proposal_id → 404
16 """
17 from __future__ import annotations
18
19 import pytest
20 from httpx import AsyncClient
21 from sqlalchemy.ext.asyncio import AsyncSession
22
23 from muse.core.types import now_utc_iso
24 from musehub.core.genesis import compute_identity_id, compute_proposal_id, compute_repo_id
25 from musehub.db.musehub_repo_models import MusehubRepo
26 from musehub.db.musehub_social_models import MusehubProposal
27
28
29 # ---------------------------------------------------------------------------
30 # Seed helpers
31 # ---------------------------------------------------------------------------
32
33
34 async def _make_repo(
35 db: AsyncSession,
36 owner: str = "proposaldev",
37 slug: str = "proposal-ssr-album",
38 ) -> str:
39 """Seed a public repo and return its repo_id string."""
40 from datetime import datetime, timezone
41 created_at = datetime.now(tz=timezone.utc)
42 owner_id = compute_identity_id(owner.encode())
43 repo_id = compute_repo_id(owner_id, slug, "code", created_at.isoformat())
44 repo = MusehubRepo(
45 repo_id=repo_id,
46 name=slug,
47 owner=owner,
48 slug=slug,
49 visibility="public",
50 owner_user_id=owner_id,
51 created_at=created_at,
52 updated_at=created_at,
53 )
54 db.add(repo)
55 await db.commit()
56 await db.refresh(repo)
57 return str(repo.repo_id)
58
59
60 async def _make_proposal(
61 db: AsyncSession,
62 repo_id: str,
63 *,
64 proposal_number: int = 1,
65 title: str = "Add bossa nova bridge",
66 body: str = "Adds a new bossa nova bridge section.",
67 state: str = "open",
68 from_branch: str = "feat/bossa-nova",
69 to_branch: str = "main",
70 author: str = "beatmaker",
71 ) -> MusehubProposal:
72 """Seed a proposal and return the ORM object."""
73 from datetime import datetime, timezone
74 author_id = compute_identity_id(author.encode())
75 proposal = MusehubProposal(
76 proposal_id=compute_proposal_id(repo_id, author_id, from_branch, to_branch, now_utc_iso()),
77 repo_id=repo_id,
78 proposal_number=proposal_number,
79 title=title,
80 body=body,
81 state=state,
82 from_branch=from_branch,
83 to_branch=to_branch,
84 author=author,
85 )
86 db.add(proposal)
87 await db.commit()
88 await db.refresh(proposal)
89 return proposal
90
91
92 # ---------------------------------------------------------------------------
93 # Proposal list SSR tests
94 # ---------------------------------------------------------------------------
95
96
97 async def test_proposal_list_renders_title_server_side(
98 client: AsyncClient,
99 db_session: AsyncSession,
100 ) -> None:
101 """Proposal title is rendered into the HTML response server-side without client JS."""
102 repo_id = await _make_repo(db_session)
103 await _make_proposal(db_session, repo_id, title="Funk bridge with wah pedal")
104 response = await client.get("/proposaldev/proposal-ssr-album/proposals")
105 assert response.status_code == 200
106 assert "text/html" in response.headers["content-type"]
107 assert "Funk bridge with wah pedal" in response.text
108
109
110 async def test_proposal_list_open_closed_counts_in_tabs(
111 client: AsyncClient,
112 db_session: AsyncSession,
113 ) -> None:
114 """State tabs display SSR-computed open/merged/closed counts."""
115 repo_id = await _make_repo(db_session)
116 await _make_proposal(db_session, repo_id, proposal_number=1, title="Open proposal 1", state="open")
117 await _make_proposal(db_session, repo_id, proposal_number=2, title="Open proposal 2", state="open")
118 await _make_proposal(db_session, repo_id, proposal_number=3, title="Merged proposal", state="merged")
119 response = await client.get("/proposaldev/proposal-ssr-album/proposals")
120 assert response.status_code == 200
121 body = response.text
122 # Tab counts for open and merged must appear as server-rendered numbers.
123 assert "2" in body # open_count
124 assert "1" in body # merged_count
125
126
127 async def test_proposal_list_htmx_fragment_on_tab_switch(
128 client: AsyncClient,
129 db_session: AsyncSession,
130 ) -> None:
131 """HX-Request: true with state=merged returns a bare HTML fragment."""
132 repo_id = await _make_repo(db_session)
133 await _make_proposal(db_session, repo_id, title="Merged feature", state="merged")
134 response = await client.get(
135 "/proposaldev/proposal-ssr-album/proposals?state=merged",
136 headers={"HX-Request": "true"},
137 )
138 assert response.status_code == 200
139 body = response.text
140 # Fragment must NOT contain the full HTML page shell.
141 assert "<html" not in body
142 assert "<head" not in body
143 # Proposal title must appear in the fragment.
144 assert "Merged feature" in body
145
146
147 # ---------------------------------------------------------------------------
148 # Proposal detail SSR tests
149 # ---------------------------------------------------------------------------
150
151
152 async def test_proposal_detail_renders_title_server_side(
153 client: AsyncClient,
154 db_session: AsyncSession,
155 ) -> None:
156 """Proposal title and branch info appear in the detail page HTML server-side."""
157 repo_id = await _make_repo(db_session)
158 proposal = await _make_proposal(
159 db_session, repo_id, title="Add jazz chord voicings", from_branch="feat/jazz"
160 )
161 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
162 assert response.status_code == 200
163 assert "text/html" in response.headers["content-type"]
164 assert "Add jazz chord voicings" in response.text
165
166
167 async def test_proposal_detail_renders_diff_stats(
168 client: AsyncClient,
169 db_session: AsyncSession,
170 ) -> None:
171 """Branch names (from_branch / to_branch) appear in the detail page HTML."""
172 repo_id = await _make_repo(db_session)
173 proposal = await _make_proposal(
174 db_session,
175 repo_id,
176 title="Bass groove proposal",
177 from_branch="feat/bass-groove",
178 to_branch="dev",
179 )
180 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
181 assert response.status_code == 200
182 body = response.text
183 # Both branch names must appear in the server-rendered HTML.
184 assert "feat/bass-groove" in body
185 assert "dev" in body
186
187
188 async def test_proposal_detail_shows_cli_hint(
189 client: AsyncClient,
190 db_session: AsyncSession,
191 ) -> None:
192 """The proposal detail page shows CLI merge hints instead of write-capable HTMX forms."""
193 repo_id = await _make_repo(db_session)
194 proposal = await _make_proposal(db_session, repo_id, title="CLI-hint proposal", state="open")
195 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
196 assert response.status_code == 200
197 body = response.text
198 # MSign is stateless — no write forms; CLI hint must be present instead.
199 assert "hx-post" not in body
200 assert "muse hub proposal" in body.lower()
201
202
203 async def test_proposal_detail_unknown_number_404(
204 client: AsyncClient,
205 db_session: AsyncSession,
206 ) -> None:
207 """A request for a non-existent proposal id returns HTTP 404."""
208 await _make_repo(db_session)
209 response = await client.get(
210 "/proposaldev/proposal-ssr-album/proposals/nonexistent-proposal-id"
211 )
212 assert response.status_code == 404
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago