"""SSR tests for the commit diff page. The diff page must render all file diffs server-side — no "Loading diff…" spinner in the initial HTML response. This file drives the implementation of SSR diff rendering in ui_commits.py::diff_page. Test matrix ----------- test_diff_page_renders_without_loading_spinner The response must NOT contain the loading-state text ("Loading diff…"). test_diff_page_renders_stats_bar The response must contain the stats bar element (.df3-stats-bar). test_diff_page_added_file_shows_plus_lines An added file's diff lines have "+" signs rendered in the HTML. test_diff_page_removed_file_shows_minus_lines A removed file's diff lines have "−" signs rendered in the HTML. test_diff_page_modified_file_shows_cohen_hunk_header A modified file's hunk header includes a Cohen action label (e.g. "[change: inserted]" or "[change: modified]"). test_diff_page_modified_file_shows_add_and_del_lines A modified file has both "+" and "-" diff lines in the HTML. test_diff_page_root_commit_no_parent_shows_added_file A root commit (no parent) still renders the diff for the added file. test_diff_page_no_changes_shows_empty_state A commit with no snapshot diffs renders the empty-state element. test_diff_page_returns_200_for_unknown_commit An unknown commit_id returns 200 (page shell) without 500. """ from __future__ import annotations import secrets from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from musehub.core.genesis import compute_branch_id, compute_identity_id, compute_repo_id from musehub.db.musehub_repo_models import MusehubBranch, MusehubCommit, MusehubCommitRef, MusehubRepo from musehub.types.json_types import StrDict # ── Constants ───────────────────────────────────────────────────────────────── _OWNER = "diffssrowner" _SLUG = "diff-ssr-repo" _SHA_PARENT = "pp" + "0" * 62 _SHA_COMMIT = "cc" + "0" * 62 _SNAP_PARENT = "sp" + "0" * 62 _SNAP_COMMIT = "sc" + "0" * 62 # object IDs stored in fake manifests _OID_OLD = "sha256:" + "a" * 64 _OID_NEW = "sha256:" + "b" * 64 _OLD_CONTENT = b"line one\nline two\nline three\n" _NEW_CONTENT = b"line one\nline two modified\nline three\nline four inserted\n" # ── Seed helpers ────────────────────────────────────────────────────────────── async def _seed_repo(db: AsyncSession) -> str: owner_id = compute_identity_id(_OWNER.encode()) created_at = datetime.now(tz=timezone.utc) repo = MusehubRepo( repo_id=compute_repo_id(owner_id, _SLUG, "code", created_at.isoformat()), name=_SLUG, owner=_OWNER, slug=_SLUG, visibility="public", owner_user_id=owner_id, created_at=created_at, updated_at=created_at, ) db.add(repo) await db.flush() return str(repo.repo_id) async def _seed_commit_pair(db: AsyncSession, repo_id: str) -> None: """Seed a parent commit and a child commit, both with snapshot IDs.""" db.add(MusehubCommit( commit_id=_SHA_PARENT, branch="main", parent_ids=[], message="initial", author="gabriel", timestamp=datetime.now(tz=timezone.utc), snapshot_id=_SNAP_PARENT, )) db.add(MusehubCommitRef(repo_id=repo_id, commit_id=_SHA_PARENT)) db.add(MusehubCommit( commit_id=_SHA_COMMIT, branch="main", parent_ids=[_SHA_PARENT], message="feat: add feature", author="gabriel", timestamp=datetime.now(tz=timezone.utc), snapshot_id=_SNAP_COMMIT, )) db.add(MusehubCommitRef(repo_id=repo_id, commit_id=_SHA_COMMIT)) db.add(MusehubBranch( branch_id=compute_branch_id(repo_id, "main"), repo_id=repo_id, name="main", head_commit_id=_SHA_COMMIT, )) await db.flush() async def _seed_root_commit(db: AsyncSession, repo_id: str) -> str: """Seed a single root commit (no parent) with a snapshot.""" sha = "rr" + "0" * 62 db.add(MusehubCommit( commit_id=sha, branch="main", parent_ids=[], message="root commit", author="gabriel", timestamp=datetime.now(tz=timezone.utc), snapshot_id=_SNAP_COMMIT, )) db.add(MusehubCommitRef(repo_id=repo_id, commit_id=sha)) db.add(MusehubBranch( branch_id=compute_branch_id(repo_id, "main"), repo_id=repo_id, name="main", head_commit_id=sha, )) await db.flush() return sha # ── Manifest + storage mock helpers ─────────────────────────────────────────── def _make_manifests( *, added: bool = False, removed: bool = False, modified: bool = False, ) -> tuple[StrDict, StrDict]: """Return (old_manifest, new_manifest) for the requested scenario.""" old: dict[str, str] = {} new: dict[str, str] = {} if added: new["src/added.py"] = _OID_NEW if removed: old["src/removed.py"] = _OID_OLD if modified: old["src/changed.py"] = _OID_OLD new["src/changed.py"] = _OID_NEW return old, new def _storage_backend(*, old_bytes: bytes = _OLD_CONTENT, new_bytes: bytes = _NEW_CONTENT) -> None: """Return a mock storage backend whose get_batch returns the two objects.""" backend = MagicMock() backend.get_batch = AsyncMock(return_value={ _OID_OLD: old_bytes, _OID_NEW: new_bytes, }) return backend # ── Patch context manager helper ────────────────────────────────────────────── def _patch_manifests(old: StrDict, new: StrDict) -> None: """Patch get_snapshot_manifest keyed on snap_id constants.""" async def _fake_manifest(_session: AsyncSession, snap_id: str) -> StrDict: # _SNAP_COMMIT → new manifest; _SNAP_PARENT → old manifest return new if snap_id == _SNAP_COMMIT else old return patch( "musehub.services.musehub_snapshot.get_snapshot_manifest", side_effect=_fake_manifest, ) # ── Tests ────────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_diff_page_renders_without_loading_spinner( client: AsyncClient, db_session: AsyncSession, ) -> None: """The initial HTML must not contain the loading spinner text.""" repo_id = await _seed_repo(db_session) await _seed_commit_pair(db_session, repo_id) await db_session.commit() old_m, new_m = _make_manifests(modified=True) with _patch_manifests(old_m, new_m), \ patch("musehub.storage.backends.get_backend", return_value=_storage_backend()): resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{_SHA_COMMIT}/diff") assert resp.status_code == 200 assert "Loading diff" not in resp.text @pytest.mark.asyncio async def test_diff_page_renders_stats_bar( client: AsyncClient, db_session: AsyncSession, ) -> None: """The stats bar element must be present in the server-rendered HTML.""" repo_id = await _seed_repo(db_session) await _seed_commit_pair(db_session, repo_id) await db_session.commit() old_m, new_m = _make_manifests(modified=True) with _patch_manifests(old_m, new_m), \ patch("musehub.storage.backends.get_backend", return_value=_storage_backend()): resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{_SHA_COMMIT}/diff") assert resp.status_code == 200 assert "df3-stats-bar" in resp.text @pytest.mark.asyncio async def test_diff_page_added_file_shows_plus_lines( client: AsyncClient, db_session: AsyncSession, ) -> None: """An added file's content is rendered with '+' lines in the diff.""" repo_id = await _seed_repo(db_session) await _seed_commit_pair(db_session, repo_id) await db_session.commit() old_m, new_m = _make_manifests(added=True) with _patch_manifests(old_m, new_m), \ patch("musehub.storage.backends.get_backend", return_value=_storage_backend( old_bytes=b"", new_bytes=_NEW_CONTENT )): resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{_SHA_COMMIT}/diff") assert resp.status_code == 200 # File path present assert "src/added.py" in resp.text # At least one '+' diff line assert "df3-dl-add" in resp.text @pytest.mark.asyncio async def test_diff_page_removed_file_shows_minus_lines( client: AsyncClient, db_session: AsyncSession, ) -> None: """A removed file is rendered with '−' lines in the diff.""" repo_id = await _seed_repo(db_session) await _seed_commit_pair(db_session, repo_id) await db_session.commit() old_m, new_m = _make_manifests(removed=True) with _patch_manifests(old_m, new_m), \ patch("musehub.storage.backends.get_backend", return_value=_storage_backend( old_bytes=_OLD_CONTENT, new_bytes=b"" )): resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{_SHA_COMMIT}/diff") assert resp.status_code == 200 assert "src/removed.py" in resp.text assert "df3-dl-del" in resp.text @pytest.mark.asyncio async def test_diff_page_modified_file_shows_cohen_hunk_header( client: AsyncClient, db_session: AsyncSession, ) -> None: """A modified file's hunk header carries a Cohen action label.""" repo_id = await _seed_repo(db_session) await _seed_commit_pair(db_session, repo_id) await db_session.commit() old_m, new_m = _make_manifests(modified=True) with _patch_manifests(old_m, new_m), \ patch("musehub.storage.backends.get_backend", return_value=_storage_backend()): resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{_SHA_COMMIT}/diff") assert resp.status_code == 200 # Cohen label: [change: inserted], [change: modified], or [change: deleted] assert "[change:" in resp.text @pytest.mark.asyncio async def test_diff_page_modified_file_shows_add_and_del_lines( client: AsyncClient, db_session: AsyncSession, ) -> None: """A modified file produces both addition and deletion diff rows.""" repo_id = await _seed_repo(db_session) await _seed_commit_pair(db_session, repo_id) await db_session.commit() old_m, new_m = _make_manifests(modified=True) with _patch_manifests(old_m, new_m), \ patch("musehub.storage.backends.get_backend", return_value=_storage_backend()): resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{_SHA_COMMIT}/diff") assert resp.status_code == 200 assert "df3-dl-add" in resp.text assert "df3-dl-del" in resp.text @pytest.mark.asyncio async def test_diff_page_root_commit_no_parent_shows_added_file( client: AsyncClient, db_session: AsyncSession, ) -> None: """Root commit (no parent snapshot) renders all files as added.""" repo_id = await _seed_repo(db_session) sha = await _seed_root_commit(db_session, repo_id) await db_session.commit() new_m = {"src/new_file.py": _OID_NEW} async def _fake_manifest(_session: AsyncSession, snap_id: str) -> StrDict: return new_m with patch("musehub.services.musehub_snapshot.get_snapshot_manifest", side_effect=_fake_manifest), \ patch("musehub.storage.backends.get_backend", return_value=_storage_backend(old_bytes=b"", new_bytes=_NEW_CONTENT)): resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{sha}/diff") assert resp.status_code == 200 assert "src/new_file.py" in resp.text assert "df3-dl-add" in resp.text @pytest.mark.asyncio async def test_diff_page_no_changes_shows_empty_state( client: AsyncClient, db_session: AsyncSession, ) -> None: """A commit with identical snapshots renders the empty-state element.""" repo_id = await _seed_repo(db_session) await _seed_commit_pair(db_session, repo_id) await db_session.commit() # Both manifests identical → no added/modified/removed same_m = {"src/stable.py": _OID_OLD} async def _fake_manifest(_session: AsyncSession, snap_id: str) -> StrDict: return same_m with patch("musehub.services.musehub_snapshot.get_snapshot_manifest", side_effect=_fake_manifest): resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{_SHA_COMMIT}/diff") assert resp.status_code == 200 assert "df3-empty" in resp.text @pytest.mark.asyncio async def test_diff_page_returns_200_for_unknown_commit( client: AsyncClient, db_session: AsyncSession, ) -> None: """An unknown commit ID returns a 200 page shell — never a 500.""" repo_id = await _seed_repo(db_session) # No commit seeded — repo exists but commit does not await db_session.commit() unknown_sha = "ee" + "0" * 62 resp = await client.get(f"/{_OWNER}/{_SLUG}/commits/{unknown_sha}/diff") assert resp.status_code == 200