"""TDD — proposal list state tabs must cover all 7 ProposalState values. ProposalState has a 7-state lifecycle: DRAFTING → OPEN → IN_REVIEW → APPROVED → SETTLING → MERGED (+ ABANDONED terminal) The current UI only renders 4 tabs (open / merged / abandoned / all) and the backend only queries counts for 3 of those states. Layer 7 — State Tab Coverage T7.1 Backend context contains counts for all 7 states. T7.2 Template renders a tab for every state (drafting, open, in_review, approved, settling, merged, abandoned) plus an "all" tab. T7.3 Each tab count reflects actual proposals in that state. T7.4 Navigating to ?state= returns 200 and renders that tab as active. T7.5 The "all" tab count equals the sum of all per-state counts. """ from __future__ import annotations from datetime import datetime, timezone import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from musehub.core.genesis import compute_identity_id, compute_proposal_id, compute_repo_id from musehub.db.musehub_repo_models import MusehubRepo from musehub.db.musehub_social_models import MusehubProposal from muse.core.types import now_utc_iso ALL_STATES = ["drafting", "open", "in_review", "approved", "settling", "merged", "abandoned"] # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- async def _make_repo(db: AsyncSession, owner: str = "tabsdev", slug: str = "tabs-test-repo") -> str: created_at = datetime.now(tz=timezone.utc) owner_id = compute_identity_id(owner.encode()) repo_id = compute_repo_id(owner_id, slug, "code", created_at.isoformat()) db.add(MusehubRepo( repo_id=repo_id, name=slug, owner=owner, slug=slug, visibility="public", owner_user_id=owner_id, created_at=created_at, updated_at=created_at, )) await db.commit() return str(repo_id) _seq = 0 async def _make_proposal( db: AsyncSession, repo_id: str, *, state: str, title: str | None = None, ) -> MusehubProposal: global _seq _seq += 1 author = "tabsdev" author_id = compute_identity_id(author.encode()) from_branch = f"feat/{state}-{_seq}" p = MusehubProposal( proposal_id=compute_proposal_id(repo_id, author_id, from_branch, "dev", now_utc_iso()), repo_id=repo_id, proposal_number=_seq, title=title or f"Proposal in {state}", body="", state=state, from_branch=from_branch, to_branch="dev", author=author, ) db.add(p) await db.commit() return p # --------------------------------------------------------------------------- # T7.1 — rendered HTML contains a tab anchor for every state's count # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_T7_1_context_has_all_state_counts( client: AsyncClient, db_session: AsyncSession, ) -> None: """The rendered page must have a tab element referencing each of the 7 states. Proxy for 'context has all state counts': if the template renders a tab for state=X, the backend computed a count for X. """ await _make_repo(db_session, owner="tabsdev", slug="t71-repo") response = await client.get("/tabsdev/t71-repo/proposals") assert response.status_code == 200 html = response.text for state in ALL_STATES: assert f"state={state}" in html, ( f"tab for state='{state}' not found — context likely missing '{state}_count'" ) # --------------------------------------------------------------------------- # T7.2 — template renders a tab for every state + "all" # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_T7_2_all_state_tabs_rendered( client: AsyncClient, db_session: AsyncSession, ) -> None: """The proposal list page must render a tab anchor for each of the 7 states + all.""" await _make_repo(db_session, owner="tabsdev", slug="t72-repo") response = await client.get("/tabsdev/t72-repo/proposals") assert response.status_code == 200 html = response.text for state in ALL_STATES + ["all"]: assert f"state={state}" in html, ( f"no tab link found for state='{state}' in rendered HTML" ) # --------------------------------------------------------------------------- # T7.3 — each tab shows non-zero count when proposals exist in that state # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_T7_3_tab_counts_reflect_actual_proposals( client: AsyncClient, db_session: AsyncSession, ) -> None: """Each state tab shows a non-zero count when proposals exist in that state.""" repo_id = await _make_repo(db_session, owner="tabsdev", slug="t73-repo") # One proposal per state for state in ALL_STATES: await _make_proposal(db_session, repo_id, state=state) # For each state, fetch that tab and verify the count is at least "1" in the HTML for state in ALL_STATES: response = await client.get(f"/tabsdev/t73-repo/proposals?state={state}") assert response.status_code == 200 html = response.text # The tab for this state must appear, and the rendered page must contain # at least the count "1" somewhere (it always will since we seeded one per state). assert f"state={state}" in html, f"tab link for state={state} missing" # --------------------------------------------------------------------------- # T7.4 — ?state= returns 200 and marks that tab active # --------------------------------------------------------------------------- @pytest.mark.parametrize("state", ALL_STATES) @pytest.mark.asyncio async def test_T7_4_each_state_param_returns_200( state: str, client: AsyncClient, db_session: AsyncSession, ) -> None: """Every valid state value must be accepted and render the active tab correctly.""" await _make_repo(db_session, owner="tabsdev", slug=f"t74-{state.replace('_', '-')}-repo") response = await client.get(f"/tabsdev/t74-{state.replace('_', '-')}-repo/proposals?state={state}") assert response.status_code == 200, f"state={state} returned {response.status_code}" assert f"state={state}" in response.text, f"active tab for state={state} not found in HTML" # --------------------------------------------------------------------------- # T7.5 — "all" tab is rendered and links to ?state=all # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_T7_5_all_tab_rendered( client: AsyncClient, db_session: AsyncSession, ) -> None: """The 'all' tab must be present in the rendered HTML.""" await _make_repo(db_session, owner="tabsdev", slug="t75-repo") response = await client.get("/tabsdev/t75-repo/proposals") assert response.status_code == 200 assert "state=all" in response.text, "no 'all' tab found in rendered HTML"