gabriel / musehub public
test_musehub_ui_commits_ssr.py python
209 lines 6.8 KB
Raw
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 8 days ago
1 """SSR + HTMX fragment tests for the MuseHub commits list page — issue #570.
2
3 Validates that commit data is rendered server-side into HTML (no JS required)
4 and that HTMX fragment requests return bare HTML without the full page shell.
5
6 Covers GET /{owner}/{repo_slug}/commits:
7
8 - test_commits_page_renders_commit_message_server_side
9 Seed a commit; its message appears in the response HTML.
10
11 - test_commits_page_filter_form_has_hx_get
12 The filter form has hx-get attribute pointing at the commits URL.
13
14 - test_commits_page_fragment_on_htmx_request
15 GET with HX-Request: true returns a bare fragment (no <html>/<head> shell).
16
17 - test_commits_page_author_filter_narrows_results
18 ?author=alice shows only Alice's commits; Bob's are absent.
19
20 - test_commits_page_pagination_renders_next
21 More than per_page commits → "Older →" link present in the response.
22 """
23 from __future__ import annotations
24
25 import secrets
26 from datetime import datetime, timezone
27
28 import pytest
29 from httpx import AsyncClient
30 from sqlalchemy.ext.asyncio import AsyncSession
31
32 from musehub.core.genesis import compute_identity_id, compute_repo_id
33 from musehub.db.musehub_repo_models import MusehubBranch, MusehubCommit, MusehubCommitRef, MusehubRepo
34
35 # ── Constants ──────────────────────────────────────────────────────────────────
36
37 _OWNER = "ssr570owner"
38 _SLUG = "ssr570-commits"
39 _SHA_ALICE = "aa" + "0" * 38
40 _SHA_BOB = "bb" + "0" * 38
41
42
43 # ── Seed helpers ───────────────────────────────────────────────────────────────
44
45
46 async def _seed_repo(db: AsyncSession) -> str:
47 """Seed a public repo and return its repo_id string."""
48 owner_id = compute_identity_id(_OWNER.encode())
49 created_at = datetime.now(tz=timezone.utc)
50 repo = MusehubRepo(
51 repo_id=compute_repo_id(owner_id, _SLUG, "code", created_at.isoformat()),
52 name=_SLUG,
53 owner=_OWNER,
54 slug=_SLUG,
55 visibility="public",
56 owner_user_id=owner_id,
57 created_at=created_at,
58 updated_at=created_at,
59 )
60 db.add(repo)
61 await db.flush()
62 return str(repo.repo_id)
63
64
65 async def _seed_commit(
66 db: AsyncSession,
67 repo_id: str,
68 *,
69 commit_id: str | None = None,
70 author: str = "alice",
71 message: str = "Test commit message",
72 branch: str = "main",
73 timestamp: datetime | None = None,
74 ) -> MusehubCommit:
75 """Seed a commit row and return the ORM object."""
76 cid = commit_id or secrets.token_hex(20)
77 ts = timestamp or datetime.now(timezone.utc)
78 commit = MusehubCommit(
79 commit_id=cid,
80 branch=branch,
81 parent_ids=[],
82 message=message,
83 author=author,
84 timestamp=ts,
85 snapshot_id=None,
86 )
87 db.add(commit)
88 db.add(MusehubCommitRef(repo_id=repo_id, commit_id=cid))
89 await db.flush()
90 return commit
91
92
93 async def _seed_branch(db: AsyncSession, repo_id: str, head_id: str, name: str = "main") -> None:
94 """Seed a branch row."""
95 db.add(MusehubBranch(repo_id=repo_id, name=name, head_commit_id=head_id))
96 await db.flush()
97
98
99 # ── Tests ──────────────────────────────────────────────────────────────────────
100
101
102 async def test_commits_page_renders_commit_message_server_side(
103 client: AsyncClient,
104 db_session: AsyncSession,
105 ) -> None:
106 """Commit message is present in the HTML response — no client JS required."""
107 repo_id = await _seed_repo(db_session)
108 await _seed_commit(
109 db_session, repo_id, message="Bassline groove at 120 BPM feels right"
110 )
111 await db_session.commit()
112
113 response = await client.get(f"/{_OWNER}/{_SLUG}/commits")
114
115 assert response.status_code == 200
116 assert "text/html" in response.headers["content-type"]
117 assert "Bassline groove at 120 BPM feels right" in response.text
118
119
120 async def test_commits_page_filter_form_has_hx_get(
121 client: AsyncClient,
122 db_session: AsyncSession,
123 ) -> None:
124 """The filter form carries hx-get so HTMX intercepts submissions."""
125 repo_id = await _seed_repo(db_session)
126 await _seed_commit(db_session, repo_id)
127 await db_session.commit()
128
129 response = await client.get(f"/{_OWNER}/{_SLUG}/commits")
130
131 assert response.status_code == 200
132 assert "hx-get" in response.text
133
134
135 async def test_commits_page_fragment_on_htmx_request(
136 client: AsyncClient,
137 db_session: AsyncSession,
138 ) -> None:
139 """HX-Request: true returns a bare HTML fragment without the full page shell."""
140 repo_id = await _seed_repo(db_session)
141 await _seed_commit(
142 db_session, repo_id, message="Fragment-only commit row"
143 )
144 await db_session.commit()
145
146 response = await client.get(
147 f"/{_OWNER}/{_SLUG}/commits",
148 headers={"HX-Request": "true"},
149 )
150
151 assert response.status_code == 200
152 # No full-page HTML shell in a fragment response.
153 assert "<html" not in response.text
154 assert "<head" not in response.text
155 # The commit content must still be present.
156 assert "Fragment-only commit row" in response.text
157
158
159 async def test_commits_page_author_filter_narrows_results(
160 client: AsyncClient,
161 db_session: AsyncSession,
162 ) -> None:
163 """?author=alice includes only Alice's commits; Bob's message is absent."""
164 repo_id = await _seed_repo(db_session)
165 await _seed_commit(
166 db_session, repo_id,
167 commit_id=_SHA_ALICE,
168 author="alice",
169 message="Alice lays down the bass",
170 )
171 await _seed_commit(
172 db_session, repo_id,
173 commit_id=_SHA_BOB,
174 author="bob",
175 message="Bob adds a reverb tail",
176 )
177 await db_session.commit()
178
179 response = await client.get(
180 f"/{_OWNER}/{_SLUG}/commits?author=alice"
181 )
182
183 assert response.status_code == 200
184 body = response.text
185 assert "Alice lays down the bass" in body
186 assert "Bob adds a reverb tail" not in body
187
188
189 async def test_commits_page_pagination_renders_next(
190 client: AsyncClient,
191 db_session: AsyncSession,
192 ) -> None:
193 """When total commits exceed per_page, the 'Older →' pagination link appears."""
194 repo_id = await _seed_repo(db_session)
195 # Seed 35 commits — more than the default per_page=30.
196 for i in range(35):
197 cid = f"{i:040x}"
198 await _seed_commit(
199 db_session, repo_id,
200 commit_id=cid,
201 message=f"Commit number {i}",
202 )
203 await db_session.commit()
204
205 response = await client.get(f"/{_OWNER}/{_SLUG}/commits")
206
207 assert response.status_code == 200
208 # "Older →" appears as an anchor when there is a next page.
209 assert "Older" in response.text
File History 13 commits
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 11 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago