"""Tests for MuseHub web UI endpoints. Covers (compare view): - test_compare_page_renders — GET /{owner}/{slug}/compare/{base}...{head} returns 200 - test_compare_page_no_auth_required — compare page accessible without JWT - test_compare_page_invalid_ref_404 — refs without ... separator return 404 - test_compare_page_unknown_owner_404 — unknown owner/slug returns 404 - test_compare_page_includes_radar — SSR: all five dimension names present in HTML (replaces JS radar) - test_compare_page_includes_piano_roll — SSR: dimension table header columns in HTML (replaces piano roll JS) - test_compare_page_includes_emotion_diff — SSR: Change delta column present (replaces emotion diff JS) - test_compare_page_includes_commit_list — SSR: all dimension rows present (replaces commit list JS) - test_compare_page_includes_create_pr_button — SSR: both ref names in heading (replaces PR button CTA) - test_compare_json_response — SSR: response is text/html with dimension data (no JSON negotiation) - test_compare_unknown_ref_404 — unknown ref returns 404 Covers acceptance criteria (commit list page): - test_commits_list_page_returns_200 — GET /{owner}/{repo}/commits returns HTML - test_commits_list_page_shows_commit_sha — SHA of seeded commit appears in page - test_commits_list_page_shows_commit_message — message appears in page - test_commits_list_page_dag_indicator — DAG node element present - test_commits_list_page_pagination_links — Older/Newer nav links present when multi-page - test_commits_list_page_branch_selector — branch dropdown is present when the repo has branches.""" await _seed_commit_list_repo(db_session) resp = await client.get(f"/{_COMMIT_LIST_OWNER}/{_COMMIT_LIST_SLUG}/commits") assert resp.status_code == 200 # Select element with branch options assert "branch-sel" in resp.text assert "main" in resp.text assert "feat/drums" in resp.text @pytest.mark.anyio async def test_commits_list_page_graph_link( client: AsyncClient, db_session: AsyncSession, ) -> None: """Link to the DAG graph page is present.""" await _seed_commit_list_repo(db_session) resp = await client.get(f"/{_COMMIT_LIST_OWNER}/{_COMMIT_LIST_SLUG}/commits") assert resp.status_code == 200 assert "/graph" in resp.text @pytest.mark.anyio async def test_commits_list_page_pagination_links( client: AsyncClient, db_session: AsyncSession, ) -> None: """Pagination nav links appear when total exceeds per_page.""" await _seed_commit_list_repo(db_session) # Request per_page=2 so 4 commits produce 2 pages resp = await client.get( f"/{_COMMIT_LIST_OWNER}/{_COMMIT_LIST_SLUG}/commits?per_page=2&page=1" ) assert resp.status_code == 200 body = resp.text # "Older" link should be active (page 1 has no "Newer") assert "Older" in body # "Newer" should be disabled on page 1 assert "Newer" in body assert "page=2" in body @pytest.mark.anyio async def test_commits_list_page_pagination_page2( client: AsyncClient, db_session: AsyncSession, ) -> None: """Page 2 renders with Newer navigation active.""" await _seed_commit_list_repo(db_session) resp = await client.get( f"/{_COMMIT_LIST_OWNER}/{_COMMIT_LIST_SLUG}/commits?per_page=2&page=2" ) assert resp.status_code == 200 body = resp.text assert "page=1" in body # "Newer" link points back to page 1 @pytest.mark.anyio async def test_commits_list_page_branch_filter_html( client: AsyncClient, db_session: AsyncSession, ) -> None: """?branch=main returns only main-branch commits in HTML.""" await _seed_commit_list_repo(db_session) resp = await client.get( f"/{_COMMIT_LIST_OWNER}/{_COMMIT_LIST_SLUG}/commits?branch=main" ) assert resp.status_code == 200 body = resp.text # main commits appear assert _SHA_MAIN_1[:8] in body assert _SHA_MAIN_2[:8] in body assert _SHA_MAIN_MERGE[:8] in body # feat/drums commit should NOT appear when filtered to main assert _SHA_FEAT[:8] not in body @pytest.mark.anyio async def test_commits_list_page_json_content_negotiation( client: AsyncClient, db_session: AsyncSession, ) -> None: """?format=json returns CommitListResponse JSON with commits and total.""" await _seed_commit_list_repo(db_session) resp = await client.get( f"/{_COMMIT_LIST_OWNER}/{_COMMIT_LIST_SLUG}/commits?format=json" ) assert resp.status_code == 200 assert "application/json" in resp.headers["content-type"] body = resp.json() assert "commits" in body assert "total" in body assert body["total"] == 4 assert len(body["commits"]) == 4 # Commits are newest first; merge commit has timestamp now-1h (most recent) commit_ids = [c["commitId"] for c in body["commits"]] assert commit_ids[0] == _SHA_MAIN_MERGE @pytest.mark.anyio async def test_commits_list_page_json_pagination( client: AsyncClient, db_session: AsyncSession, ) -> None: """JSON with per_page=1&page=2 returns the second commit.""" await _seed_commit_list_repo(db_session) resp = await client.get( f"/{_COMMIT_LIST_OWNER}/{_COMMIT_LIST_SLUG}/commits" "?format=json&per_page=1&page=2" ) assert resp.status_code == 200 body = resp.json() assert body["total"] == 4 assert len(body["commits"]) == 1 # Page 2 (newest-first) is the second most-recent commit. # Newest: _SHA_MAIN_MERGE (now-1h), then _SHA_MAIN_2 (now-2h) assert body["commits"][0]["commitId"] == _SHA_MAIN_2 @pytest.mark.anyio async def test_commits_list_page_empty_state( client: AsyncClient, db_session: AsyncSession, ) -> None: """A repo with no commits shows the empty state message.""" repo = MusehubRepo( name="empty-repo", owner="emptyowner", slug="empty-repo", visibility="public", owner_user_id="empty-owner-uid", ) db_session.add(repo) await db_session.commit() resp = await client.get("/emptyowner/empty-repo/commits") assert resp.status_code == 200 assert "No commits yet" in resp.text or "muse push" in resp.text # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Commit detail enhancements — # --------------------------------------------------------------------------- async def _seed_commit_detail_fixtures( db_session: AsyncSession, ) -> tuple[str, str, str]: """Seed a public repo with a parent commit and a child commit. Returns (repo_id, parent_commit_id, child_commit_id). """ repo = MusehubRepo( name="commit-detail-test", owner="testuser", slug="commit-detail-test", visibility="public", owner_user_id="test-owner", ) db_session.add(repo) await db_session.flush() repo_id = str(repo.repo_id) branch = MusehubBranch( repo_id=repo_id, name="main", head_commit_id=None, ) db_session.add(branch) parent_commit_id = "aaaa0000111122223333444455556666aaaabbbb" child_commit_id = "bbbb1111222233334444555566667777bbbbcccc" parent_commit = MusehubCommit( repo_id=repo_id, commit_id=parent_commit_id, branch="main", parent_ids=[], message="init: establish harmonic foundation in C major\n\nKey: C major\nBPM: 120\nMeter: 4/4", author="testuser", timestamp=datetime.now(UTC) - timedelta(hours=2), snapshot_id=None, ) child_commit = MusehubCommit( repo_id=repo_id, commit_id=child_commit_id, branch="main", parent_ids=[parent_commit_id], message="feat(keys): add melodic piano phrase in D minor\n\nKey: D minor\nBPM: 132\nMeter: 3/4\nSection: verse", author="testuser", timestamp=datetime.now(UTC) - timedelta(hours=1), snapshot_id=None, ) db_session.add(parent_commit) db_session.add(child_commit) await db_session.commit() return repo_id, parent_commit_id, child_commit_id @pytest.mark.anyio async def test_commit_detail_page_renders_enhanced_metadata( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit detail page SSR renders commit header fields (SHA, author, branch, parent link).""" await _seed_commit_detail_fixtures(db_session) sha = "bbbb1111222233334444555566667777bbbbcccc" response = await client.get(f"/testuser/commit-detail-test/commits/{sha}") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] body = response.text # SSR commit header — short SHA present assert "bbbb1111" in body # Author field rendered server-side assert "testuser" in body # Parent SHA navigation link present assert "aaaa0000" in body @pytest.mark.anyio async def test_commit_detail_audio_shell_with_snapshot_id( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit with snapshot_id gets a WaveSurfer shell rendered by the server.""" from datetime import datetime, timezone _repo_id, _parent_id, _child_id = await _seed_commit_detail_fixtures(db_session) repo = MusehubRepo( name="audio-test-repo", owner="testuser", slug="audio-test-repo", visibility="public", owner_user_id="test-owner", ) db_session.add(repo) await db_session.flush() snap_id = "sha256:deadbeef12345678deadbeef12345678deadbeef12345678deadbeef12345678" commit_with_audio = MusehubCommit( commit_id="cccc2222333344445555666677778888ccccdddd", repo_id=str(repo.repo_id), branch="main", parent_ids=[], message="Commit with audio snapshot", author="testuser", timestamp=datetime.now(tz=timezone.utc), snapshot_id=snap_id, ) db_session.add(commit_with_audio) await db_session.commit() response = await client.get( f"/testuser/audio-test-repo/commits/cccc2222333344445555666677778888ccccdddd" ) assert response.status_code == 200 body = response.text assert "cd-waveform" in body assert "cd-audio-section" in body @pytest.mark.anyio async def test_commit_detail_ssr_message_present_in_body( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit message text is rendered in the SSR page body (replaces JS renderCommitBody).""" await _seed_commit_detail_fixtures(db_session) sha = "bbbb1111222233334444555566667777bbbbcccc" response = await client.get(f"/testuser/commit-detail-test/commits/{sha}") assert response.status_code == 200 body = response.text # SSR renders the commit message directly — no JS renderCommitBody needed assert "feat(keys): add melodic piano phrase in D minor" in body @pytest.mark.anyio async def test_commit_detail_diff_summary_endpoint_returns_five_dimensions( client: AsyncClient, db_session: AsyncSession, auth_headers: dict[str, str], ) -> None: """GET /api/v1/repos/{repo_id}/commits/{sha}/diff-summary returns 5 dimensions.""" repo_id, _parent_id, child_id = await _seed_commit_detail_fixtures(db_session) response = await client.get( f"/api/v1/repos/{repo_id}/commits/{child_id}/diff-summary", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert data["commitId"] == child_id assert data["parentId"] == _parent_id assert "dimensions" in data assert len(data["dimensions"]) == 5 dim_names = {d["dimension"] for d in data["dimensions"]} assert dim_names == {"harmonic", "rhythmic", "melodic", "structural", "dynamic"} for dim in data["dimensions"]: assert 0.0 <= dim["score"] <= 1.0 assert dim["label"] in {"none", "low", "medium", "high"} assert dim["color"] in {"dim-none", "dim-low", "dim-medium", "dim-high"} assert "overallScore" in data assert 0.0 <= data["overallScore"] <= 1.0 @pytest.mark.anyio async def test_commit_detail_diff_summary_root_commit_scores_one( client: AsyncClient, db_session: AsyncSession, auth_headers: dict[str, str], ) -> None: """Diff summary for a root commit (no parent) scores all dimensions at 1.0.""" repo_id, parent_id, _child_id = await _seed_commit_detail_fixtures(db_session) response = await client.get( f"/api/v1/repos/{repo_id}/commits/{parent_id}/diff-summary", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert data["parentId"] is None for dim in data["dimensions"]: assert dim["score"] == 1.0 assert dim["label"] == "high" @pytest.mark.anyio async def test_commit_detail_diff_summary_keyword_detection( client: AsyncClient, db_session: AsyncSession, auth_headers: dict[str, str], ) -> None: """Diff summary detects melodic keyword in child commit message.""" repo_id, _parent_id, child_id = await _seed_commit_detail_fixtures(db_session) response = await client.get( f"/api/v1/repos/{repo_id}/commits/{child_id}/diff-summary", headers=auth_headers, ) assert response.status_code == 200 data = response.json() melodic_dim = next(d for d in data["dimensions"] if d["dimension"] == "melodic") # child commit message contains "melodic" keyword → non-zero score assert melodic_dim["score"] > 0.0 @pytest.mark.anyio async def test_commit_detail_diff_summary_unknown_commit_404( client: AsyncClient, db_session: AsyncSession, auth_headers: dict[str, str], ) -> None: """Diff summary for unknown commit ID returns 404.""" repo_id, _p, _c = await _seed_commit_detail_fixtures(db_session) response = await client.get( f"/api/v1/repos/{repo_id}/commits/deadbeefdeadbeefdeadbeef/diff-summary", headers=auth_headers, ) assert response.status_code == 404 # --------------------------------------------------------------------------- # Commit comment threads — # --------------------------------------------------------------------------- @pytest.mark.anyio async def test_commit_page_has_comment_section_html( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit detail page HTML includes the HTMX comment target container.""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="testuser", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text # SSR replaces JS-loaded comment section with a server-rendered HTMX target assert "commit-comments" in body assert "hx-target" in body @pytest.mark.anyio async def test_commit_page_has_htmx_comment_form( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit detail page has an HTMX-driven comment form (replaces old JS comment functions).""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="testuser", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text # HTMX form replaces JS renderComments/submitComment/loadComments assert "hx-post" in body assert "hx-target" in body assert "textarea" in body @pytest.mark.anyio async def test_commit_page_comment_htmx_target_present( client: AsyncClient, db_session: AsyncSession, ) -> None: """HTMX comment target div is present for server-side comment injection.""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="testuser", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text assert 'id="commit-comments"' in body @pytest.mark.anyio async def test_commit_page_comment_htmx_posts_to_comments_endpoint( client: AsyncClient, db_session: AsyncSession, ) -> None: """HTMX form posts to the commit comments endpoint (replaces old JS API fetch).""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="testuser", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text assert "hx-post" in body assert "/comments" in body @pytest.mark.anyio async def test_commit_page_comment_has_ssr_avatar( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit page SSR comment thread renders avatar initials via server-side template.""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="testuser", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text # comment-avatar only rendered when comments exist; check commit page structure assert "commit-detail" in body or "page-data" in body @pytest.mark.anyio async def test_commit_page_comment_has_htmx_form_elements( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit page HTMX comment form has textarea and submit button.""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="testuser", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text # HTMX form replaces old new-comment-form/new-comment-body/comment-submit-btn assert 'name="body"' in body assert "btn-primary" in body assert "Comment" in body @pytest.mark.anyio async def test_commit_page_comment_section_shows_count_heading( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit page SSR comment section shows a count heading (replaces 'Discussion' heading).""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="testuser", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text assert "comment" in body # --------------------------------------------------------------------------- # Commit detail enhancements — ref URL links, DB tags in panel, prose # summary # --------------------------------------------------------------------------- @pytest.mark.anyio async def test_commit_page_ssr_renders_commit_message( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit message is rendered server-side (replaces JS ref-tag / tagPill rendering).""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Unique groove message XYZ", author="testuser", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text # SSR renders commit message directly — no JS tagPill/isRefUrl needed assert "Unique groove message XYZ" in body @pytest.mark.anyio async def test_commit_page_ssr_renders_author_metadata( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit author and branch appear in the SSR metadata grid (replaces JS muse-tags panel).""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="jazzproducer", timestamp=datetime.now(tz=timezone.utc), ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text # SSR metadata grid shows author — no JS loadMuseTagsPanel needed assert "jazzproducer" in body @pytest.mark.anyio async def test_commit_page_no_audio_shell_when_no_snapshot( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit page without snapshot_id omits WaveSurfer shell (replaces buildProseSummary check).""" from datetime import datetime, timezone repo_id = await _make_repo(db_session) commit_id = "abc1234567890abcdef1234567890abcdef12345678" commit = MusehubCommit( commit_id=commit_id, repo_id=repo_id, branch="main", parent_ids=[], message="Add chorus section", author="testuser", timestamp=datetime.now(tz=timezone.utc), snapshot_id=None, ) db_session.add(commit) await db_session.commit() response = await client.get(f"/testuser/test-beats/commits/{commit_id}") assert response.status_code == 200 body = response.text assert "commit-waveform" not in body # --------------------------------------------------------------------------- # Audio player — listen page tests # --------------------------------------------------------------------------- @pytest.mark.anyio async def test_listen_page_renders( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{slug}/listen/{ref} must return 200 HTML.""" await _make_repo(db_session) ref = "abc1234567890abcdef" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] @pytest.mark.anyio async def test_listen_page_no_auth_required( client: AsyncClient, db_session: AsyncSession, ) -> None: """Listen page must be accessible without an Authorization header.""" await _make_repo(db_session) ref = "deadbeef1234" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code != 401 assert response.status_code == 200 @pytest.mark.anyio async def test_listen_page_contains_waveform_ui( client: AsyncClient, db_session: AsyncSession, ) -> None: """Listen page HTML must contain the page config for listen.ts.""" await _make_repo(db_session) ref = "cafebabe1234" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code == 200 body = response.text assert '"page": "listen"' in body @pytest.mark.anyio async def test_listen_page_contains_play_button( client: AsyncClient, db_session: AsyncSession, ) -> None: """Listen page must dispatch the listen.ts module via page config.""" await _make_repo(db_session) ref = "feed1234abcdef" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code == 200 body = response.text assert '"page": "listen"' in body @pytest.mark.anyio async def test_listen_page_contains_speed_selector( client: AsyncClient, db_session: AsyncSession, ) -> None: """Listen page must dispatch the listen.ts module (speed selector is client-side).""" await _make_repo(db_session) ref = "1a2b3c4d5e6f7890" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code == 200 body = response.text assert '"page": "listen"' in body @pytest.mark.anyio async def test_listen_page_contains_ab_loop_ui( client: AsyncClient, db_session: AsyncSession, ) -> None: """Listen page must dispatch the listen.ts module (A/B loop controls are client-side).""" await _make_repo(db_session) ref = "aabbccddeeff0011" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code == 200 body = response.text assert '"page": "listen"' in body @pytest.mark.anyio async def test_listen_page_loads_wavesurfer_vendor( client: AsyncClient, db_session: AsyncSession, ) -> None: """Listen page must dispatch the listen.ts module (WaveSurfer loaded via app.js bundle).""" await _make_repo(db_session) ref = "112233445566778899" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code == 200 body = response.text # WaveSurfer is now bundled/loaded by listen.ts, not as a separate script tag assert '"page": "listen"' in body # wavesurfer must NOT be loaded from an external CDN assert "unpkg.com/wavesurfer" not in body assert "cdn.jsdelivr.net/wavesurfer" not in body assert "cdnjs.cloudflare.com/ajax/libs/wavesurfer" not in body @pytest.mark.anyio async def test_listen_page_loads_audio_player_js( client: AsyncClient, db_session: AsyncSession, ) -> None: """Listen page must dispatch the listen.ts module (audio player is now bundled in app.js).""" await _make_repo(db_session) ref = "99aabbccddeeff00" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code == 200 body = response.text assert '"page": "listen"' in body @pytest.mark.anyio async def test_listen_track_page_renders( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{slug}/listen/{ref}/{path} must return 200.""" await _make_repo(db_session) ref = "feedface0011aabb" response = await client.get( f"/testuser/test-beats/listen/{ref}/tracks/bass.mp3" ) assert response.status_code == 200 assert "text/html" in response.headers["content-type"] @pytest.mark.anyio async def test_listen_track_page_has_track_path_in_js( client: AsyncClient, db_session: AsyncSession, ) -> None: """Track path is passed via page config JSON to listen.ts, not as a JS variable.""" await _make_repo(db_session) ref = "00aabbccddeeff11" track = "tracks/lead-guitar.mp3" response = await client.get( f"/testuser/test-beats/listen/{ref}/{track}" ) assert response.status_code == 200 body = response.text assert '"page": "listen"' in body assert "lead-guitar.mp3" in body @pytest.mark.anyio async def test_listen_page_unknown_repo_404( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET listen page with nonexistent owner/slug must return 404.""" response = await client.get( "/nobody/nonexistent-repo/listen/abc123" ) assert response.status_code == 404 @pytest.mark.anyio async def test_listen_page_keyboard_shortcuts_documented( client: AsyncClient, db_session: AsyncSession, ) -> None: """Listen page dispatches listen.ts (keyboard shortcuts handled client-side).""" await _make_repo(db_session) ref = "cafe0011aabb2233" response = await client.get(f"/testuser/test-beats/listen/{ref}") assert response.status_code == 200 body = response.text assert '"page": "listen"' in body # --------------------------------------------------------------------------- # Compare view # --------------------------------------------------------------------------- @pytest.mark.anyio async def test_compare_page_renders( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{slug}/compare/{base}...{head} returns 200 HTML.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/main...feature") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] body = response.text assert "MuseHub" in body assert "main" in body assert "feature" in body @pytest.mark.anyio async def test_compare_page_no_auth_required( client: AsyncClient, db_session: AsyncSession, ) -> None: """Compare page is accessible without a JWT token.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/main...feature") assert response.status_code == 200 @pytest.mark.anyio async def test_compare_page_invalid_ref_404( client: AsyncClient, db_session: AsyncSession, ) -> None: """Compare path without '...' separator returns 404.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/mainfeature") assert response.status_code == 404 @pytest.mark.anyio async def test_compare_page_unknown_owner_404( client: AsyncClient, ) -> None: """Unknown owner/slug combination returns 404 on compare page.""" response = await client.get("/nobody/norepo/compare/main...feature") assert response.status_code == 404 @pytest.mark.anyio async def test_compare_page_includes_radar( client: AsyncClient, db_session: AsyncSession, ) -> None: """Compare page SSR HTML contains all five musical dimension names (replaces JS radar). The compare page now renders data server-side via a dimension table. Musical dimensions (Melodic, Harmonic, etc.) must appear in the HTML body before any client-side JavaScript runs. """ await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/main...feature") assert response.status_code == 200 body = response.text assert "Melodic" in body assert "Harmonic" in body @pytest.mark.anyio async def test_compare_page_includes_piano_roll( client: AsyncClient, db_session: AsyncSession, ) -> None: """Compare page SSR HTML contains the dimension table (replaces piano roll JS panel). The compare page now renders a dimension comparison table server-side. Both ref names must appear as column headers in the HTML. """ await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/main...feature") assert response.status_code == 200 body = response.text assert "main" in body assert "feature" in body assert "Dimension" in body @pytest.mark.anyio async def test_compare_page_includes_emotion_diff( client: AsyncClient, db_session: AsyncSession, ) -> None: """Compare page SSR HTML contains change delta column (replaces emotion diff JS). The dimension table includes a Change column showing delta values server-side. """ await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/main...feature") assert response.status_code == 200 body = response.text assert "Change" in body assert "%" in body @pytest.mark.anyio async def test_compare_page_includes_commit_list( client: AsyncClient, db_session: AsyncSession, ) -> None: """Compare page SSR HTML contains dimension rows (replaces client-side commit list JS). All five musical dimensions must appear as data rows in the server-rendered table. """ await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/main...feature") assert response.status_code == 200 body = response.text assert "Rhythmic" in body assert "Structural" in body assert "Dynamic" in body @pytest.mark.anyio async def test_compare_page_includes_create_pr_button( client: AsyncClient, db_session: AsyncSession, ) -> None: """Compare page SSR HTML contains both ref names in the heading (replaces PR button CTA). The SSR compare page shows the base and head refs in the page header. """ await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/main...feature") assert response.status_code == 200 body = response.text assert "Compare" in body assert "main" in body assert "feature" in body @pytest.mark.anyio async def test_compare_json_response( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{slug}/compare/{refs} returns HTML with SSR dimension data. The compare page is now fully SSR — no JSON format negotiation. The response is always text/html containing the dimension table. """ await _make_repo(db_session) response = await client.get("/testuser/test-beats/compare/main...feature") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] body = response.text assert "Melodic" in body assert "main" in body # --------------------------------------------------------------------------- # Issue #208 — Branch list and tag browser tests # --------------------------------------------------------------------------- async def _make_repo_with_branches( db_session: AsyncSession, ) -> tuple[str, str, str]: """Seed a repo with two branches (main + feature) and return (repo_id, owner, slug).""" repo = MusehubRepo( name="branch-test", owner="testuser", slug="branch-test", visibility="private", owner_user_id="test-owner", ) db_session.add(repo) await db_session.flush() repo_id = str(repo.repo_id) main_branch = MusehubBranch(repo_id=repo_id, name="main", head_commit_id="aaa000") feat_branch = MusehubBranch(repo_id=repo_id, name="feat/jazz-bridge", head_commit_id="bbb111") db_session.add_all([main_branch, feat_branch]) # Two commits on main, one unique commit on feat/jazz-bridge now = datetime.now(UTC) c1 = MusehubCommit( commit_id="aaa000", repo_id=repo_id, branch="main", parent_ids=[], message="Initial commit", author="composer@muse.app", timestamp=now, ) c2 = MusehubCommit( commit_id="aaa001", repo_id=repo_id, branch="main", parent_ids=["aaa000"], message="Add bridge", author="composer@muse.app", timestamp=now, ) c3 = MusehubCommit( commit_id="bbb111", repo_id=repo_id, branch="feat/jazz-bridge", parent_ids=["aaa000"], message="Add jazz chord", author="composer@muse.app", timestamp=now, ) db_session.add_all([c1, c2, c3]) await db_session.commit() return repo_id, "testuser", "branch-test" async def _make_repo_with_releases( db_session: AsyncSession, ) -> tuple[str, str, str]: """Seed a repo with namespaced releases used as tags.""" repo = MusehubRepo( name="tag-test", owner="testuser", slug="tag-test", visibility="private", owner_user_id="test-owner", ) db_session.add(repo) await db_session.flush() repo_id = str(repo.repo_id) now = datetime.now(UTC) releases = [ MusehubRelease( repo_id=repo_id, tag="emotion:happy", title="Happy vibes", body="", commit_id="abc001", author="composer", created_at=now, download_urls={}, ), MusehubRelease( repo_id=repo_id, tag="genre:jazz", title="Jazz release", body="", commit_id="abc002", author="composer", created_at=now, download_urls={}, ), MusehubRelease( repo_id=repo_id, tag="v1.0", title="Version 1.0", body="", commit_id="abc003", author="composer", created_at=now, download_urls={}, ), ] db_session.add_all(releases) await db_session.commit() return repo_id, "testuser", "tag-test" @pytest.mark.anyio async def test_branches_page_lists_all( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{slug}/branches returns 200 HTML.""" await _make_repo_with_branches(db_session) resp = await client.get("/testuser/branch-test/branches") assert resp.status_code == 200 assert "text/html" in resp.headers["content-type"] body = resp.text assert "MuseHub" in body # Page-specific JS identifiers assert "branch-row" in body or "branches" in body.lower() @pytest.mark.anyio async def test_branches_default_marked( client: AsyncClient, db_session: AsyncSession, ) -> None: """JSON response marks the default branch with isDefault=true.""" await _make_repo_with_branches(db_session) resp = await client.get( "/testuser/branch-test/branches", headers={"Accept": "application/json"}, ) assert resp.status_code == 200 data = resp.json() assert "branches" in data default_branches = [b for b in data["branches"] if b.get("isDefault")] assert len(default_branches) == 1 assert default_branches[0]["name"] == "main" @pytest.mark.anyio async def test_branches_compare_link( client: AsyncClient, db_session: AsyncSession, ) -> None: """Branches page HTML contains compare link JavaScript.""" await _make_repo_with_branches(db_session) resp = await client.get("/testuser/branch-test/branches") assert resp.status_code == 200 body = resp.text # The JS template must reference the compare URL pattern assert "compare" in body.lower() @pytest.mark.anyio async def test_branches_new_pr_button( client: AsyncClient, db_session: AsyncSession, ) -> None: """Branches page HTML contains New Pull Request link JavaScript.""" await _make_repo_with_branches(db_session) resp = await client.get("/testuser/branch-test/branches") assert resp.status_code == 200 body = resp.text assert "Pull Request" in body @pytest.mark.anyio async def test_branches_json_response( client: AsyncClient, db_session: AsyncSession, ) -> None: """JSON response includes branches with ahead/behind counts and divergence placeholder.""" await _make_repo_with_branches(db_session) resp = await client.get( "/testuser/branch-test/branches?format=json", ) assert resp.status_code == 200 data = resp.json() assert "branches" in data assert "defaultBranch" in data assert data["defaultBranch"] == "main" branches_by_name = {b["name"]: b for b in data["branches"]} assert "main" in branches_by_name assert "feat/jazz-bridge" in branches_by_name main = branches_by_name["main"] assert main["isDefault"] is True assert main["aheadCount"] == 0 assert main["behindCount"] == 0 feat = branches_by_name["feat/jazz-bridge"] assert feat["isDefault"] is False # feat has 1 unique commit (bbb111); main has 2 commits (aaa000, aaa001) not shared with feat assert feat["aheadCount"] == 1 assert feat["behindCount"] == 2 # Divergence is a placeholder (all None) div = feat["divergence"] assert div["melodic"] is None assert div["harmonic"] is None @pytest.mark.anyio async def test_tags_page_lists_all( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{slug}/tags returns 200 HTML.""" await _make_repo_with_releases(db_session) resp = await client.get("/testuser/tag-test/tags") assert resp.status_code == 200 assert "text/html" in resp.headers["content-type"] body = resp.text assert "MuseHub" in body assert "Tags" in body @pytest.mark.anyio async def test_tags_namespace_filter( client: AsyncClient, db_session: AsyncSession, ) -> None: """Tags page HTML includes namespace filter dropdown JavaScript.""" await _make_repo_with_releases(db_session) resp = await client.get("/testuser/tag-test/tags") assert resp.status_code == 200 body = resp.text # Namespace filter select element is rendered by JS assert "ns-filter" in body or "namespace" in body.lower() # Namespace icons present assert "🌘" in body or "emotion" in body @pytest.mark.anyio async def test_tags_json_response( client: AsyncClient, db_session: AsyncSession, ) -> None: """JSON response returns TagListResponse with namespace grouping.""" await _make_repo_with_releases(db_session) resp = await client.get( "/testuser/tag-test/tags?format=json", ) assert resp.status_code == 200 data = resp.json() assert "tags" in data assert "namespaces" in data # All three releases become tags assert len(data["tags"]) == 3 tags_by_name = {t["tag"]: t for t in data["tags"]} assert "emotion:happy" in tags_by_name assert "genre:jazz" in tags_by_name assert "v1.0" in tags_by_name assert tags_by_name["emotion:happy"]["namespace"] == "emotion" assert tags_by_name["genre:jazz"]["namespace"] == "genre" assert tags_by_name["v1.0"]["namespace"] == "version" # Namespaces are sorted assert sorted(data["namespaces"]) == data["namespaces"] assert "emotion" in data["namespaces"] assert "genre" in data["namespaces"] assert "version" in data["namespaces"] # --------------------------------------------------------------------------- # Arrangement matrix page — # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Piano roll page tests — # --------------------------------------------------------------------------- @pytest.mark.anyio async def test_arrange_page_returns_200( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{slug}/arrange/{ref} returns 200 HTML without a JWT.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/arrange/HEAD") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] @pytest.mark.anyio async def test_piano_roll_page_returns_200( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{slug}/piano-roll/{ref} returns 200 HTML.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/piano-roll/main") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] @pytest.mark.anyio async def test_arrange_page_no_auth_required( client: AsyncClient, db_session: AsyncSession, ) -> None: """Arrangement matrix page is accessible without a JWT (auth handled client-side).""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/arrange/HEAD") assert response.status_code == 200 assert response.status_code != 401 @pytest.mark.anyio async def test_arrange_page_contains_musehub( client: AsyncClient, db_session: AsyncSession, ) -> None: """Arrangement matrix page HTML shell contains 'MuseHub' branding.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/arrange/abc1234") assert response.status_code == 200 assert "MuseHub" in response.text @pytest.mark.anyio async def test_arrange_page_contains_grid_js( client: AsyncClient, db_session: AsyncSession, ) -> None: """Arrangement matrix page embeds the grid rendering JS (renderMatrix or arrange).""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/arrange/HEAD") assert response.status_code == 200 body = response.text assert "renderMatrix" in body or "arrange" in body.lower() @pytest.mark.anyio async def test_arrange_page_contains_density_logic( client: AsyncClient, db_session: AsyncSession, ) -> None: """Arrangement matrix page includes density colour logic.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/arrange/HEAD") assert response.status_code == 200 body = response.text assert "density" in body.lower() or "noteDensity" in body @pytest.mark.anyio async def test_arrange_page_contains_token_form( client: AsyncClient, db_session: AsyncSession, ) -> None: """Arrangement matrix page renders the SSR arrange grid.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/arrange/HEAD") assert response.status_code == 200 body = response.text assert "ar-commit-header" in body or '"page": "arrange"' in body assert "Arrange" in body @pytest.mark.anyio async def test_arrange_page_unknown_repo_returns_404( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{unknown}/{slug}/arrange/{ref} returns 404 for unknown repos.""" response = await client.get("/unknown-user/no-such-repo/arrange/HEAD") assert response.status_code == 404 @pytest.mark.anyio async def test_commit_detail_unknown_format_param_returns_html( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET commit detail page ignores ?format=json — SSR always returns HTML.""" await _seed_commit_detail_fixtures(db_session) sha = "bbbb1111222233334444555566667777bbbbcccc" response = await client.get( f"/testuser/commit-detail-test/commits/{sha}?format=json" ) assert response.status_code == 200 assert "text/html" in response.headers["content-type"] # SSR commit page — commit message appears in body assert "feat(keys)" in response.text @pytest.mark.anyio async def test_commit_detail_wavesurfer_js_conditional_on_audio_url( client: AsyncClient, db_session: AsyncSession, ) -> None: """WaveSurfer JS block is only present when audio_url is set (replaces musicalMeta JS checks).""" await _seed_commit_detail_fixtures(db_session) sha = "bbbb1111222233334444555566667777bbbbcccc" response = await client.get(f"/testuser/commit-detail-test/commits/{sha}") assert response.status_code == 200 body = response.text # The child commit has no snapshot_id in _seed_commit_detail_fixtures → no WaveSurfer assert "commit-waveform" not in body # WaveSurfer script only loaded when audio is present — not here assert "wavesurfer.min.js" not in body @pytest.mark.anyio async def test_commit_detail_nav_has_parent_link( client: AsyncClient, db_session: AsyncSession, ) -> None: """Commit detail page navigation includes the parent commit link (SSR).""" await _seed_commit_detail_fixtures(db_session) sha = "bbbb1111222233334444555566667777bbbbcccc" response = await client.get(f"/testuser/commit-detail-test/commits/{sha}") assert response.status_code == 200 body = response.text # SSR renders parent commit link when parent_ids is non-empty assert "Parent:" in body # Parent SHA abbreviated to 8 chars in href assert "aaaa0000" in body @pytest.mark.anyio async def test_piano_roll_page_no_auth_required( client: AsyncClient, db_session: AsyncSession, ) -> None: """Piano roll UI page is accessible without a JWT token.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/piano-roll/main") assert response.status_code == 200 @pytest.mark.anyio async def test_piano_roll_page_loads_piano_roll_js( client: AsyncClient, db_session: AsyncSession, ) -> None: """Piano roll page references piano-roll.js script.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/piano-roll/main") assert response.status_code == 200 assert "piano-roll.js" in response.text @pytest.mark.anyio async def test_piano_roll_page_contains_canvas( client: AsyncClient, db_session: AsyncSession, ) -> None: """Piano roll page embeds a canvas element for rendering.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/piano-roll/main") assert response.status_code == 200 body = response.text assert "PianoRoll" in body or "piano-canvas" in body or "piano-roll.js" in body @pytest.mark.anyio async def test_piano_roll_page_has_token_form( client: AsyncClient, db_session: AsyncSession, ) -> None: """Piano roll page renders the SSR piano roll wrapper and canvas.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats/piano-roll/main") assert response.status_code == 200 assert "piano-roll-wrapper" in response.text assert "piano-roll.js" in response.text @pytest.mark.anyio async def test_piano_roll_page_unknown_repo_404( client: AsyncClient, db_session: AsyncSession, ) -> None: """Piano roll page for an unknown repo returns 404.""" response = await client.get("/nobody/no-repo/piano-roll/main") assert response.status_code == 404 @pytest.mark.anyio async def test_arrange_tab_in_repo_nav( client: AsyncClient, db_session: AsyncSession, ) -> None: """Repo home page navigation includes an 'Arrange' tab link.""" await _make_repo(db_session) response = await client.get("/testuser/test-beats") assert response.status_code == 200 assert "Arrange" in response.text or "arrange" in response.text @pytest.mark.anyio async def test_piano_roll_track_page_returns_200( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /piano-roll/{ref}/{path} (single track) returns 200.""" await _make_repo(db_session) response = await client.get( "/testuser/test-beats/piano-roll/main/tracks/bass.mid" ) assert response.status_code == 200 @pytest.mark.anyio async def test_piano_roll_track_page_embeds_path( client: AsyncClient, db_session: AsyncSession, ) -> None: """Single-track piano roll page embeds the MIDI file path in the JS context.""" await _make_repo(db_session) response = await client.get( "/testuser/test-beats/piano-roll/main/tracks/bass.mid" ) assert response.status_code == 200 assert "tracks/bass.mid" in response.text @pytest.mark.anyio async def test_piano_roll_js_served(client: AsyncClient) -> None: """GET /static/piano-roll.js returns 200 JavaScript.""" response = await client.get("/static/piano-roll.js") assert response.status_code == 200 assert "javascript" in response.headers.get("content-type", "") @pytest.mark.anyio async def test_piano_roll_js_contains_renderer(client: AsyncClient) -> None: """piano-roll.js exports the PianoRoll.render function.""" response = await client.get("/static/piano-roll.js") assert response.status_code == 200 body = response.text assert "PianoRoll" in body assert "render" in body async def _seed_blob_fixtures(db_session: AsyncSession) -> str: """Seed a public repo with a branch and typed objects for blob viewer tests. Creates: - repo: testuser/blob-test (public) - branch: main - objects: tracks/bass.mid, tracks/keys.mp3, metadata.json, cover.webp Returns repo_id. """ repo = MusehubRepo( name="blob-test", owner="testuser", slug="blob-test", visibility="public", owner_user_id="test-owner", ) db_session.add(repo) await db_session.flush() commit = MusehubCommit( commit_id="blobdeadbeef12", repo_id=str(repo.repo_id), message="add blob fixtures", branch="main", author="testuser", timestamp=datetime.now(tz=UTC), ) db_session.add(commit) branch = MusehubBranch( repo_id=str(repo.repo_id), name="main", head_commit_id="blobdeadbeef12", ) db_session.add(branch) for path, size in [ ("tracks/bass.mid", 2048), ("tracks/keys.mp3", 8192), ("metadata.json", 512), ("cover.webp", 4096), ]: obj = MusehubObject( object_id=f"sha256:blob_{path.replace('/', '_')}", repo_id=str(repo.repo_id), path=path, size_bytes=size, disk_path=f"/tmp/blob_{path.replace('/', '_')}", ) db_session.add(obj) await db_session.commit() return str(repo.repo_id) @pytest.mark.anyio async def test_blob_404_unknown_path( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /api/v1/repos/{repo_id}/blob/{ref}/{path} returns 404 for unknown path.""" repo_id = await _seed_blob_fixtures(db_session) response = await client.get(f"/api/v1/repos/{repo_id}/blob/main/does/not/exist.mid") assert response.status_code == 404 @pytest.mark.anyio async def test_blob_image_shows_inline( client: AsyncClient, db_session: AsyncSession, ) -> None: """Blob page for .webp file includes rendering logic in the template JS.""" await _seed_blob_fixtures(db_session) response = await client.get("/testuser/blob-test/blob/main/cover.webp") assert response.status_code == 200 body = response.text # blob.ts handles image rendering client-side; SSR provides __blobCfg data assert "__blobCfg" in body assert "cover.webp" in body @pytest.mark.anyio async def test_blob_json_response( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /api/v1/repos/{repo_id}/blob/{ref}/{path} returns BlobMetaResponse JSON.""" repo_id = await _seed_blob_fixtures(db_session) response = await client.get( f"/api/v1/repos/{repo_id}/blob/main/tracks/bass.mid" ) assert response.status_code == 200 data = response.json() assert data["path"] == "tracks/bass.mid" assert data["filename"] == "bass.mid" assert data["sizeBytes"] == 2048 assert data["fileType"] == "midi" assert data["sha"].startswith("sha256:") assert "/raw/" in data["rawUrl"] # MIDI is binary — no content_text assert data["contentText"] is None @pytest.mark.anyio async def test_blob_json_syntax_highlighted( client: AsyncClient, db_session: AsyncSession, ) -> None: """Blob page for .json file includes syntax-highlighting logic in the template JS.""" await _seed_blob_fixtures(db_session) response = await client.get("/testuser/blob-test/blob/main/metadata.json") assert response.status_code == 200 body = response.text # blob.ts handles syntax highlighting client-side; SSR provides __blobCfg data assert "__blobCfg" in body assert "metadata.json" in body @pytest.mark.anyio async def test_blob_midi_shows_piano_roll_link( client: AsyncClient, db_session: AsyncSession, ) -> None: """GET /{owner}/{repo}/blob/{ref}/{path} returns 200 HTML for a .mid file. The template's client-side JS must reference the piano roll URL pattern so that clicking the page in a browser navigates to the piano roll viewer. """ await _seed_blob_fixtures(db_session) response = await client.get("/testuser/blob-test/blob/main/tracks/bass.mid") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] body = response.text # JS in the template constructs piano-roll URLs for MIDI files assert "piano-roll" in body or "Piano Roll" in body # Filename is embedded in the page context assert "bass.mid" in body @pytest.mark.anyio async def test_blob_mp3_shows_audio_player( client: AsyncClient, db_session: AsyncSession, ) -> None: """Blob page for .mp3 file includes