gabriel / musehub public
test_proposal_state_tabs.py python
185 lines 7.0 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 """TDD — proposal list state tabs must cover all 7 ProposalState values.
2
3 ProposalState has a 7-state lifecycle:
4 DRAFTING → OPEN → IN_REVIEW → APPROVED → SETTLING → MERGED (+ ABANDONED terminal)
5
6 The current UI only renders 4 tabs (open / merged / abandoned / all) and the
7 backend only queries counts for 3 of those states.
8
9 Layer 7 — State Tab Coverage
10
11 T7.1 Backend context contains counts for all 7 states.
12 T7.2 Template renders a tab for every state (drafting, open, in_review,
13 approved, settling, merged, abandoned) plus an "all" tab.
14 T7.3 Each tab count reflects actual proposals in that state.
15 T7.4 Navigating to ?state=<any valid state> returns 200 and renders that
16 tab as active.
17 T7.5 The "all" tab count equals the sum of all per-state counts.
18 """
19 from __future__ import annotations
20
21 from datetime import datetime, timezone
22
23 import pytest
24 from httpx import AsyncClient
25 from sqlalchemy.ext.asyncio import AsyncSession
26
27 from musehub.core.genesis import compute_identity_id, compute_proposal_id, compute_repo_id
28 from musehub.db.musehub_repo_models import MusehubRepo
29 from musehub.db.musehub_social_models import MusehubProposal
30 from muse.core.types import now_utc_iso
31
32 ALL_STATES = ["drafting", "open", "in_review", "approved", "settling", "merged", "abandoned"]
33
34
35 # ---------------------------------------------------------------------------
36 # Helpers
37 # ---------------------------------------------------------------------------
38
39 async def _make_repo(db: AsyncSession, owner: str = "tabsdev", slug: str = "tabs-test-repo") -> str:
40 created_at = datetime.now(tz=timezone.utc)
41 owner_id = compute_identity_id(owner.encode())
42 repo_id = compute_repo_id(owner_id, slug, "code", created_at.isoformat())
43 db.add(MusehubRepo(
44 repo_id=repo_id, name=slug, owner=owner, slug=slug,
45 visibility="public", owner_user_id=owner_id,
46 created_at=created_at, updated_at=created_at,
47 ))
48 await db.commit()
49 return str(repo_id)
50
51
52 _seq = 0
53
54 async def _make_proposal(
55 db: AsyncSession,
56 repo_id: str,
57 *,
58 state: str,
59 title: str | None = None,
60 ) -> MusehubProposal:
61 global _seq
62 _seq += 1
63 author = "tabsdev"
64 author_id = compute_identity_id(author.encode())
65 from_branch = f"feat/{state}-{_seq}"
66 p = MusehubProposal(
67 proposal_id=compute_proposal_id(repo_id, author_id, from_branch, "dev", now_utc_iso()),
68 repo_id=repo_id,
69 proposal_number=_seq,
70 title=title or f"Proposal in {state}",
71 body="",
72 state=state,
73 from_branch=from_branch,
74 to_branch="dev",
75 author=author,
76 )
77 db.add(p)
78 await db.commit()
79 return p
80
81
82 # ---------------------------------------------------------------------------
83 # T7.1 — rendered HTML contains a tab anchor for every state's count
84 # ---------------------------------------------------------------------------
85
86 @pytest.mark.asyncio
87 async def test_T7_1_context_has_all_state_counts(
88 client: AsyncClient,
89 db_session: AsyncSession,
90 ) -> None:
91 """The rendered page must have a tab element referencing each of the 7 states.
92
93 Proxy for 'context has all state counts': if the template renders a tab for
94 state=X, the backend computed a count for X.
95 """
96 await _make_repo(db_session, owner="tabsdev", slug="t71-repo")
97 response = await client.get("/tabsdev/t71-repo/proposals")
98 assert response.status_code == 200
99 html = response.text
100
101 for state in ALL_STATES:
102 assert f"state={state}" in html, (
103 f"tab for state='{state}' not found — context likely missing '{state}_count'"
104 )
105
106
107 # ---------------------------------------------------------------------------
108 # T7.2 — template renders a tab for every state + "all"
109 # ---------------------------------------------------------------------------
110
111 @pytest.mark.asyncio
112 async def test_T7_2_all_state_tabs_rendered(
113 client: AsyncClient,
114 db_session: AsyncSession,
115 ) -> None:
116 """The proposal list page must render a tab anchor for each of the 7 states + all."""
117 await _make_repo(db_session, owner="tabsdev", slug="t72-repo")
118 response = await client.get("/tabsdev/t72-repo/proposals")
119 assert response.status_code == 200
120 html = response.text
121
122 for state in ALL_STATES + ["all"]:
123 assert f"state={state}" in html, (
124 f"no tab link found for state='{state}' in rendered HTML"
125 )
126
127
128 # ---------------------------------------------------------------------------
129 # T7.3 — each tab shows non-zero count when proposals exist in that state
130 # ---------------------------------------------------------------------------
131
132 @pytest.mark.asyncio
133 async def test_T7_3_tab_counts_reflect_actual_proposals(
134 client: AsyncClient,
135 db_session: AsyncSession,
136 ) -> None:
137 """Each state tab shows a non-zero count when proposals exist in that state."""
138 repo_id = await _make_repo(db_session, owner="tabsdev", slug="t73-repo")
139
140 # One proposal per state
141 for state in ALL_STATES:
142 await _make_proposal(db_session, repo_id, state=state)
143
144 # For each state, fetch that tab and verify the count is at least "1" in the HTML
145 for state in ALL_STATES:
146 response = await client.get(f"/tabsdev/t73-repo/proposals?state={state}")
147 assert response.status_code == 200
148 html = response.text
149 # The tab for this state must appear, and the rendered page must contain
150 # at least the count "1" somewhere (it always will since we seeded one per state).
151 assert f"state={state}" in html, f"tab link for state={state} missing"
152
153
154 # ---------------------------------------------------------------------------
155 # T7.4 — ?state=<X> returns 200 and marks that tab active
156 # ---------------------------------------------------------------------------
157
158 @pytest.mark.parametrize("state", ALL_STATES)
159 @pytest.mark.asyncio
160 async def test_T7_4_each_state_param_returns_200(
161 state: str,
162 client: AsyncClient,
163 db_session: AsyncSession,
164 ) -> None:
165 """Every valid state value must be accepted and render the active tab correctly."""
166 await _make_repo(db_session, owner="tabsdev", slug=f"t74-{state.replace('_', '-')}-repo")
167 response = await client.get(f"/tabsdev/t74-{state.replace('_', '-')}-repo/proposals?state={state}")
168 assert response.status_code == 200, f"state={state} returned {response.status_code}"
169 assert f"state={state}" in response.text, f"active tab for state={state} not found in HTML"
170
171
172 # ---------------------------------------------------------------------------
173 # T7.5 — "all" tab is rendered and links to ?state=all
174 # ---------------------------------------------------------------------------
175
176 @pytest.mark.asyncio
177 async def test_T7_5_all_tab_rendered(
178 client: AsyncClient,
179 db_session: AsyncSession,
180 ) -> None:
181 """The 'all' tab must be present in the rendered HTML."""
182 await _make_repo(db_session, owner="tabsdev", slug="t75-repo")
183 response = await client.get("/tabsdev/t75-repo/proposals")
184 assert response.status_code == 200
185 assert "state=all" in response.text, "no 'all' tab found in rendered HTML"
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago