"""TDD: Proposal detail page renders the symbol delta server-side. The main column tells a coherent story: change fingerprint (ratio bar) → file-grouped symbol delta → commits timeline. All rendered in the initial HTML, no client-side JS required to see what changed. Covers GET /{owner}/{repo_slug}/proposals/{proposal_id}: test_delta_section_present_in_html test_added_symbol_name_rendered_server_side test_modified_symbol_name_rendered_server_side test_deleted_symbol_name_rendered_server_side test_file_path_rendered_as_group_header test_sym_counts_rendered_server_side test_ratio_bar_present_when_symbols_exist test_breaking_change_marker_rendered_inline test_empty_delta_renders_empty_state test_op_sigil_present_for_each_entry test_symbol_link_points_to_symbols_page test_multiple_files_each_rendered_as_group test_added_deleted_cancelled_not_rendered """ from __future__ import annotations from datetime import datetime, timezone import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from muse.core.types import fake_id, now_utc_iso from musehub.core.genesis import compute_identity_id, compute_proposal_id, compute_repo_id from musehub.db.musehub_repo_models import MusehubCommit, MusehubCommitRef, MusehubRepo from musehub.db.musehub_social_models import MusehubProposal from musehub.types.json_types import JSONObject # --------------------------------------------------------------------------- # Seed helpers # --------------------------------------------------------------------------- async def _make_repo(db: AsyncSession, slug: str = "delta-ssr-repo") -> str: created_at = datetime.now(tz=timezone.utc) owner_id = compute_identity_id(b"deltadev") repo_id = compute_repo_id(owner_id, slug, "code", created_at.isoformat()) repo = MusehubRepo( repo_id=repo_id, name=slug, owner="deltadev", slug=slug, visibility="public", owner_user_id=owner_id, created_at=created_at, updated_at=created_at, ) db.add(repo) await db.commit() await db.refresh(repo) return str(repo.repo_id) async def _make_proposal( db: AsyncSession, repo_id: str, *, from_branch: str = "feat/test", to_branch: str = "main", title: str = "Test proposal", ) -> MusehubProposal: author_id = compute_identity_id(b"deltadev") proposal = MusehubProposal( proposal_id=compute_proposal_id(repo_id, author_id, from_branch, to_branch, now_utc_iso()), repo_id=repo_id, proposal_number=1, title=title, body="", state="open", from_branch=from_branch, to_branch=to_branch, author="deltadev", ) db.add(proposal) await db.commit() await db.refresh(proposal) return proposal async def _make_commit( db: AsyncSession, repo_id: str, *, branch: str, structured_delta: JSONObject | None = None, breaking_changes: list[str] | None = None, message: str = "feat: update symbols", ) -> str: cid = fake_id(now_utc_iso()) row = MusehubCommit( commit_id=cid, branch=branch, parent_ids=[], message=message, author="deltadev", timestamp=datetime.now(timezone.utc), structured_delta=structured_delta, breaking_changes=breaking_changes or [], ) db.add(row) db.add(MusehubCommitRef(repo_id=repo_id, commit_id=cid)) await db.commit() return cid def _delta_with(*ops: tuple[str, str]) -> JSONObject: """Build a structured_delta from (op_type, address) pairs.""" by_file: dict[str, list[dict]] = {} for op_type, address in ops: file_path = address.split("::")[0] by_file.setdefault(file_path, []).append({"op": op_type, "address": address}) return { "ops": [ {"address": fp, "child_ops": cops} for fp, cops in by_file.items() ] } # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- async def test_delta_section_present_in_html( client: AsyncClient, db_session: AsyncSession, ) -> None: """The Change Map / Symbol Delta section is present in the rendered HTML.""" repo_id = await _make_repo(db_session, "delta-section") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-section") await _make_commit(db_session, repo_id, branch="feat/delta-section", structured_delta=_delta_with(("insert", "src/a.py::Fn"))) resp = await client.get(f"/deltadev/delta-section/proposals/{proposal.proposal_id}") assert resp.status_code == 200 # The delta section heading must be present server-side. assert "Symbol Delta" in resp.text or "Change Map" in resp.text async def test_added_symbol_name_rendered_server_side( client: AsyncClient, db_session: AsyncSession, ) -> None: """An inserted symbol's name appears in the HTML without client JS.""" repo_id = await _make_repo(db_session, "delta-added") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-added") await _make_commit( db_session, repo_id, branch="feat/delta-added", structured_delta=_delta_with(("insert", "src/billing.py::compute_invoice_total")), ) resp = await client.get(f"/deltadev/delta-added/proposals/{proposal.proposal_id}") assert resp.status_code == 200 assert "compute_invoice_total" in resp.text async def test_modified_symbol_name_rendered_server_side( client: AsyncClient, db_session: AsyncSession, ) -> None: """A replaced symbol's name appears in the HTML.""" repo_id = await _make_repo(db_session, "delta-modified") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-modified") await _make_commit( db_session, repo_id, branch="feat/delta-modified", structured_delta=_delta_with(("replace", "src/auth.py::validate_session")), ) resp = await client.get(f"/deltadev/delta-modified/proposals/{proposal.proposal_id}") assert resp.status_code == 200 assert "validate_session" in resp.text async def test_deleted_symbol_name_rendered_server_side( client: AsyncClient, db_session: AsyncSession, ) -> None: """A deleted symbol's name appears in the HTML.""" repo_id = await _make_repo(db_session, "delta-deleted") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-deleted") await _make_commit( db_session, repo_id, branch="feat/delta-deleted", structured_delta=_delta_with(("delete", "src/legacy.py::old_compute")), ) resp = await client.get(f"/deltadev/delta-deleted/proposals/{proposal.proposal_id}") assert resp.status_code == 200 assert "old_compute" in resp.text async def test_file_path_rendered_as_group_header( client: AsyncClient, db_session: AsyncSession, ) -> None: """The file path appears as a group header in the delta section.""" repo_id = await _make_repo(db_session, "delta-file-header") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-file-header") await _make_commit( db_session, repo_id, branch="feat/delta-file-header", structured_delta=_delta_with(("insert", "musehub/services/billing.py::process_payment")), ) resp = await client.get(f"/deltadev/delta-file-header/proposals/{proposal.proposal_id}") assert resp.status_code == 200 assert "musehub/services/billing.py" in resp.text async def test_sym_counts_rendered_server_side( client: AsyncClient, db_session: AsyncSession, ) -> None: """Added/modified/deleted symbol counts appear as numbers in the HTML.""" repo_id = await _make_repo(db_session, "delta-counts") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-counts") await _make_commit( db_session, repo_id, branch="feat/delta-counts", structured_delta=_delta_with( ("insert", "src/a.py::NewFn"), ("replace", "src/b.py::ChangedFn"), ("delete", "src/c.py::OldFn"), ), ) resp = await client.get(f"/deltadev/delta-counts/proposals/{proposal.proposal_id}") assert resp.status_code == 200 body = resp.text # Each bucket count of 1 must appear somewhere in the HTML. assert "1" in body async def test_ratio_bar_present_when_symbols_exist( client: AsyncClient, db_session: AsyncSession, ) -> None: """The ratio bar element is present in the HTML when the delta is non-empty.""" repo_id = await _make_repo(db_session, "delta-ratio") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-ratio") await _make_commit( db_session, repo_id, branch="feat/delta-ratio", structured_delta=_delta_with(("insert", "src/a.py::Fn")), ) resp = await client.get(f"/deltadev/delta-ratio/proposals/{proposal.proposal_id}") assert resp.status_code == 200 # The ratio bar CSS class must be present. assert "prd-delta-ratio" in resp.text async def test_breaking_change_marker_rendered_inline( client: AsyncClient, db_session: AsyncSession, ) -> None: """A breaking change has its marker rendered inline next to the symbol.""" repo_id = await _make_repo(db_session, "delta-breaking") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-breaking") await _make_commit( db_session, repo_id, branch="feat/delta-breaking", structured_delta=_delta_with(("delete", "src/api.py::public_endpoint")), breaking_changes=["src/api.py::public_endpoint"], ) resp = await client.get(f"/deltadev/delta-breaking/proposals/{proposal.proposal_id}") assert resp.status_code == 200 body = resp.text assert "public_endpoint" in body # Breaking marker must appear near the symbol — check for the CSS class. assert "prd-breaking" in body async def test_empty_delta_renders_empty_state( client: AsyncClient, db_session: AsyncSession, ) -> None: """No commits with deltas → empty state message in the delta section.""" repo_id = await _make_repo(db_session, "delta-empty") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-empty") # Commit with no structured_delta. await _make_commit(db_session, repo_id, branch="feat/delta-empty", structured_delta=None) resp = await client.get(f"/deltadev/delta-empty/proposals/{proposal.proposal_id}") assert resp.status_code == 200 assert "prd-delta-empty" in resp.text async def test_op_sigil_present_for_each_entry( client: AsyncClient, db_session: AsyncSession, ) -> None: """Each symbol entry has an op sigil element (prd-op-sigil).""" repo_id = await _make_repo(db_session, "delta-sigil") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-sigil") await _make_commit( db_session, repo_id, branch="feat/delta-sigil", structured_delta=_delta_with(("insert", "src/a.py::Fn")), ) resp = await client.get(f"/deltadev/delta-sigil/proposals/{proposal.proposal_id}") assert resp.status_code == 200 assert "prd-op-sigil" in resp.text async def test_symbol_link_points_to_symbols_page( client: AsyncClient, db_session: AsyncSession, ) -> None: """Each symbol name is a link to /symbols?q=.""" repo_id = await _make_repo(db_session, "delta-link") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-link") await _make_commit( db_session, repo_id, branch="feat/delta-link", structured_delta=_delta_with(("insert", "src/a.py::find_user")), ) resp = await client.get(f"/deltadev/delta-link/proposals/{proposal.proposal_id}") assert resp.status_code == 200 assert "symbols?q=find_user" in resp.text async def test_multiple_files_each_rendered_as_group( client: AsyncClient, db_session: AsyncSession, ) -> None: """When symbols span multiple files, each file gets its own group header.""" repo_id = await _make_repo(db_session, "delta-multi-file") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-multi-file") await _make_commit( db_session, repo_id, branch="feat/delta-multi-file", structured_delta=_delta_with( ("insert", "src/billing.py::invoice"), ("replace", "src/auth.py::login"), ), ) resp = await client.get(f"/deltadev/delta-multi-file/proposals/{proposal.proposal_id}") assert resp.status_code == 200 body = resp.text assert "src/billing.py" in body assert "src/auth.py" in body async def test_added_then_deleted_symbol_not_rendered( client: AsyncClient, db_session: AsyncSession, ) -> None: """A symbol inserted and deleted in the same proposal nets to zero — not rendered.""" from datetime import timedelta repo_id = await _make_repo(db_session, "delta-cancel") proposal = await _make_proposal(db_session, repo_id, from_branch="feat/delta-cancel") t1 = datetime(2026, 1, 1, tzinfo=timezone.utc) t2 = t1 + timedelta(hours=1) cid1 = fake_id("c1-cancel") cid2 = fake_id("c2-cancel") row1 = MusehubCommit( commit_id=cid1, branch="feat/delta-cancel", parent_ids=[], message="add then remove", author="deltadev", timestamp=t1, structured_delta=_delta_with(("insert", "src/x.py::ephemeral_fn")), breaking_changes=[], ) row2 = MusehubCommit( commit_id=cid2, branch="feat/delta-cancel", parent_ids=[], message="remove ephemeral", author="deltadev", timestamp=t2, structured_delta=_delta_with(("delete", "src/x.py::ephemeral_fn")), breaking_changes=[], ) db_session.add(row1) db_session.add(MusehubCommitRef(repo_id=repo_id, commit_id=cid1)) db_session.add(row2) db_session.add(MusehubCommitRef(repo_id=repo_id, commit_id=cid2)) await db_session.commit() resp = await client.get(f"/deltadev/delta-cancel/proposals/{proposal.proposal_id}") assert resp.status_code == 200 # Symbol that was added then deleted (net zero) must not appear in delta output. # It may appear in commits timeline, but not in the symbol delta section. # We check via the sigil — if it appeared in the delta it'd have a prd-op-sigil. # The symbol name alone isn't sufficient since it could appear in commit messages. body = resp.text # Count occurrences in the delta section — there should be none with op sigil. # Simplest check: the delta section must report 0 total. assert 'data-sym-total="0"' in body or "prd-delta-empty" in body