gabriel / musehub public

test_proposal_list_phase2.py file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:0 fix: fall back to any indexed mpack in read_object_bytes when push mpac… · gabriel · Jun 17, 2026
1 """Tests for Phase 2 of issue #35 β€” list_proposals filter extensions, /heat, /readiness.
2
3 Tier 2 β€” Integration
4 Real test DB via ``db_session`` fixture. Exercises filter predicates,
5 sort orders, and the aggregate query functions against live rows.
6
7 Tier 6 β€” Performance
8 Measured assertions: each query must complete under a documented threshold
9 on the test-DB hardware. No mocks β€” all assertions hit the real DB.
10 """
11
12 from __future__ import annotations
13
14 import time
15 import uuid
16 from datetime import datetime, timezone
17
18 import pytest
19 import pytest_asyncio
20 from httpx import AsyncClient
21 from sqlalchemy import select
22 from sqlalchemy.ext.asyncio import AsyncSession
23
24 from musehub.db.musehub_identity_models import MusehubIdentity
25 from musehub.db.musehub_repo_models import MusehubRepo
26 from musehub.db.musehub_social_models import MusehubProposalReview
27 from musehub.models.musehub import ProposalListFilters
28 from musehub.services import musehub_proposals
29 from tests.factories import create_proposal
30
31
32 # ─────────────────────────────────────────────────────────────────────────────
33 # Helpers
34 # ─────────────────────────────────────────────────────────────────────────────
35
36 def _now() -> datetime:
37 return datetime.now(tz=timezone.utc)
38
39
40 async def _make_repo(session: AsyncSession, owner: str = "testuser", slug: str | None = None) -> MusehubRepo:
41 from musehub.core.genesis import compute_identity_id, compute_repo_id
42 slug = slug or f"repo-{uuid.uuid4().hex[:8]}"
43 owner_user_id = compute_identity_id(owner.encode())
44 created_at = _now()
45 repo_id = compute_repo_id(owner_user_id, slug, "code", created_at.isoformat())
46 repo = MusehubRepo(
47 repo_id=repo_id,
48 name=slug,
49 owner=owner,
50 slug=slug,
51 visibility="public",
52 owner_user_id=owner_user_id,
53 description="",
54 tags=[],
55 created_at=created_at,
56 )
57 session.add(repo)
58 await session.commit()
59 return repo
60
61
62 async def _make_identity(
63 session: AsyncSession,
64 handle: str,
65 identity_type: str = "human",
66 ) -> MusehubIdentity:
67 from musehub.core.genesis import compute_identity_id
68 iid = compute_identity_id(handle.encode())
69 identity = MusehubIdentity(
70 identity_id=iid,
71 handle=handle,
72 identity_type=identity_type,
73 )
74 session.add(identity)
75 await session.commit()
76 return identity
77
78
79 async def _make_review(
80 session: AsyncSession,
81 proposal_id: str,
82 reviewer: str,
83 state: str = "approved",
84 ) -> MusehubProposalReview:
85 from musehub.core.genesis import compute_identity_id, compute_review_id
86 reviewer_identity_id = compute_identity_id(reviewer.encode())
87 review = MusehubProposalReview(
88 review_id=compute_review_id(proposal_id, reviewer_identity_id, _now().isoformat()),
89 proposal_id=proposal_id,
90 reviewer_username=reviewer,
91 state=state,
92 body=None,
93 submitted_at=_now(),
94 created_at=_now(),
95 )
96 session.add(review)
97 await session.commit()
98 return review
99
100
101 # ─────────────────────────────────────────────────────────────────────────────
102 # Tier 2 β€” Integration: filter correctness
103 # ─────────────────────────────────────────────────────────────────────────────
104
105 class TestIntegrationListProposalsState:
106 """list_proposals state filter hits the DB and returns matching rows."""
107
108 @pytest.mark.asyncio
109 async def test_state_open_returns_only_open(self, db_session: AsyncSession) -> None:
110 repo = await _make_repo(db_session)
111 await create_proposal(db_session, repo.repo_id, state="open")
112 await create_proposal(db_session, repo.repo_id, state="merged")
113 result = await musehub_proposals.list_proposals(
114 db_session, repo.repo_id, filters=ProposalListFilters(state="open")
115 )
116 assert all(p.state == "open" for p in result.proposals)
117 assert result.total == 1
118
119 @pytest.mark.asyncio
120 async def test_state_merged_returns_only_merged(self, db_session: AsyncSession) -> None:
121 repo = await _make_repo(db_session)
122 await create_proposal(db_session, repo.repo_id, state="open")
123 await create_proposal(db_session, repo.repo_id, state="merged")
124 result = await musehub_proposals.list_proposals(
125 db_session, repo.repo_id, filters=ProposalListFilters(state="merged")
126 )
127 assert all(p.state == "merged" for p in result.proposals)
128 assert result.total == 1
129
130 @pytest.mark.asyncio
131 async def test_state_all_returns_all(self, db_session: AsyncSession) -> None:
132 repo = await _make_repo(db_session)
133 await create_proposal(db_session, repo.repo_id, state="open")
134 await create_proposal(db_session, repo.repo_id, state="merged")
135 result = await musehub_proposals.list_proposals(
136 db_session, repo.repo_id, filters=ProposalListFilters(state="all")
137 )
138 assert result.total == 2
139
140
141 class TestIntegrationListProposalsRiskBand:
142 """list_proposals risk_band filter maps score ranges to SQL predicates."""
143
144 @pytest.mark.asyncio
145 async def test_critical_band_returns_high_score_proposals(self, db_session: AsyncSession) -> None:
146 repo = await _make_repo(db_session)
147 p_critical = await create_proposal(db_session, repo.repo_id, state="open")
148 p_low = await create_proposal(db_session, repo.repo_id, state="open")
149 # Set risk scores directly
150 p_critical.risk_score = 0.9
151 p_low.risk_score = 0.1
152 await db_session.commit()
153
154 result = await musehub_proposals.list_proposals(
155 db_session, repo.repo_id,
156 filters=ProposalListFilters(state="open", risk_band=["critical"]),
157 )
158 ids = {p.proposal_id for p in result.proposals}
159 assert p_critical.proposal_id in ids
160 assert p_low.proposal_id not in ids
161
162 @pytest.mark.asyncio
163 async def test_high_band_excludes_critical(self, db_session: AsyncSession) -> None:
164 repo = await _make_repo(db_session)
165 p_critical = await create_proposal(db_session, repo.repo_id, state="open")
166 p_high = await create_proposal(db_session, repo.repo_id, state="open")
167 p_critical.risk_score = 0.8
168 p_high.risk_score = 0.6
169 await db_session.commit()
170
171 result = await musehub_proposals.list_proposals(
172 db_session, repo.repo_id,
173 filters=ProposalListFilters(state="open", risk_band=["high"]),
174 )
175 ids = {p.proposal_id for p in result.proposals}
176 assert p_high.proposal_id in ids
177 assert p_critical.proposal_id not in ids
178
179 @pytest.mark.asyncio
180 async def test_multiple_bands_or_semantics(self, db_session: AsyncSession) -> None:
181 repo = await _make_repo(db_session)
182 p_crit = await create_proposal(db_session, repo.repo_id, state="open")
183 p_low = await create_proposal(db_session, repo.repo_id, state="open")
184 p_med = await create_proposal(db_session, repo.repo_id, state="open")
185 p_crit.risk_score = 0.9
186 p_low.risk_score = 0.05
187 p_med.risk_score = 0.3
188 await db_session.commit()
189
190 result = await musehub_proposals.list_proposals(
191 db_session, repo.repo_id,
192 filters=ProposalListFilters(state="open", risk_band=["critical", "low"]),
193 )
194 ids = {p.proposal_id for p in result.proposals}
195 assert p_crit.proposal_id in ids
196 assert p_low.proposal_id in ids
197 assert p_med.proposal_id not in ids
198
199 @pytest.mark.asyncio
200 async def test_none_band_returns_zero_score_proposals(self, db_session: AsyncSession) -> None:
201 repo = await _make_repo(db_session)
202 p_zero = await create_proposal(db_session, repo.repo_id, state="open")
203 p_nonzero = await create_proposal(db_session, repo.repo_id, state="open")
204 p_zero.risk_score = 0.0
205 p_nonzero.risk_score = 0.5
206 await db_session.commit()
207
208 result = await musehub_proposals.list_proposals(
209 db_session, repo.repo_id,
210 filters=ProposalListFilters(state="open", risk_band=["none"]),
211 )
212 ids = {p.proposal_id for p in result.proposals}
213 assert p_zero.proposal_id in ids
214 assert p_nonzero.proposal_id not in ids
215
216
217 class TestIntegrationListProposalsDomain:
218 """list_proposals domain filter returns only proposals active in that domain."""
219
220 @pytest.mark.asyncio
221 async def test_code_domain_returns_proposals_with_nonzero_risk(self, db_session: AsyncSession) -> None:
222 repo = await _make_repo(db_session)
223 p_code = await create_proposal(db_session, repo.repo_id, state="open")
224 p_none = await create_proposal(db_session, repo.repo_id, state="open")
225 p_code.risk_score = 0.4
226 p_none.risk_score = 0.0
227 await db_session.commit()
228
229 result = await musehub_proposals.list_proposals(
230 db_session, repo.repo_id,
231 filters=ProposalListFilters(state="open", domain=["code"]),
232 )
233 ids = {p.proposal_id for p in result.proposals}
234 assert p_code.proposal_id in ids
235 assert p_none.proposal_id not in ids
236
237 @pytest.mark.asyncio
238 async def test_no_domain_filter_returns_all(self, db_session: AsyncSession) -> None:
239 repo = await _make_repo(db_session)
240 p1 = await create_proposal(db_session, repo.repo_id, state="open")
241 p2 = await create_proposal(db_session, repo.repo_id, state="open")
242 p1.risk_score = 0.4
243 p2.risk_score = 0.0
244 await db_session.commit()
245
246 result = await musehub_proposals.list_proposals(
247 db_session, repo.repo_id,
248 filters=ProposalListFilters(state="open"),
249 )
250 assert result.total == 2
251
252
253 class TestIntegrationListProposalsAuthorType:
254 """list_proposals author_type filter joins identities table."""
255
256 @pytest.mark.asyncio
257 async def test_agent_filter_returns_agent_proposals(self, db_session: AsyncSession) -> None:
258 repo = await _make_repo(db_session)
259 await _make_identity(db_session, "bot-1", identity_type="agent")
260 await _make_identity(db_session, "human-1", identity_type="human")
261
262 p_agent = await create_proposal(db_session, repo.repo_id, state="open", author="bot-1")
263 p_human = await create_proposal(db_session, repo.repo_id, state="open", author="human-1")
264
265 result = await musehub_proposals.list_proposals(
266 db_session, repo.repo_id,
267 filters=ProposalListFilters(state="open", author_type="agent"),
268 )
269 ids = {p.proposal_id for p in result.proposals}
270 assert p_agent.proposal_id in ids
271 assert p_human.proposal_id not in ids
272
273 @pytest.mark.asyncio
274 async def test_human_filter_returns_human_and_unknown_authors(self, db_session: AsyncSession) -> None:
275 repo = await _make_repo(db_session)
276 await _make_identity(db_session, "human-2", identity_type="human")
277
278 p_human = await create_proposal(db_session, repo.repo_id, state="open", author="human-2")
279 p_unknown = await create_proposal(db_session, repo.repo_id, state="open", author="no-identity")
280
281 result = await musehub_proposals.list_proposals(
282 db_session, repo.repo_id,
283 filters=ProposalListFilters(state="open", author_type="human"),
284 )
285 ids = {p.proposal_id for p in result.proposals}
286 assert p_human.proposal_id in ids
287 assert p_unknown.proposal_id in ids
288
289
290 class TestIntegrationListProposalsAssignedReviewer:
291 """list_proposals assigned_reviewer filter checks proposal reviews."""
292
293 @pytest.mark.asyncio
294 async def test_assigned_reviewer_returns_proposals_with_pending_review(
295 self, db_session: AsyncSession
296 ) -> None:
297 repo = await _make_repo(db_session)
298 p_assigned = await create_proposal(db_session, repo.repo_id, state="open")
299 p_other = await create_proposal(db_session, repo.repo_id, state="open")
300 await _make_review(db_session, p_assigned.proposal_id, "alice", state="pending")
301
302 result = await musehub_proposals.list_proposals(
303 db_session, repo.repo_id,
304 filters=ProposalListFilters(state="open", assigned_reviewer="alice"),
305 )
306 ids = {p.proposal_id for p in result.proposals}
307 assert p_assigned.proposal_id in ids
308 assert p_other.proposal_id not in ids
309
310 @pytest.mark.asyncio
311 async def test_dismissed_review_not_returned(self, db_session: AsyncSession) -> None:
312 repo = await _make_repo(db_session)
313 p = await create_proposal(db_session, repo.repo_id, state="open")
314 await _make_review(db_session, p.proposal_id, "alice", state="dismissed")
315
316 result = await musehub_proposals.list_proposals(
317 db_session, repo.repo_id,
318 filters=ProposalListFilters(state="open", assigned_reviewer="alice"),
319 )
320 assert result.total == 0
321
322
323 class TestIntegrationListProposalsSort:
324 """list_proposals sort orders return proposals in correct sequence."""
325
326 @pytest.mark.asyncio
327 async def test_newest_sort_returns_most_recent_first(self, db_session: AsyncSession) -> None:
328 repo = await _make_repo(db_session)
329 p1 = await create_proposal(db_session, repo.repo_id, state="open")
330 p2 = await create_proposal(db_session, repo.repo_id, state="open")
331 # Ensure p2 has later created_at
332 p2.created_at = datetime(2030, 1, 2, tzinfo=timezone.utc)
333 p1.created_at = datetime(2030, 1, 1, tzinfo=timezone.utc)
334 await db_session.commit()
335
336 result = await musehub_proposals.list_proposals(
337 db_session, repo.repo_id,
338 filters=ProposalListFilters(state="open", sort="newest"),
339 )
340 assert result.proposals[0].proposal_id == p2.proposal_id
341
342 @pytest.mark.asyncio
343 async def test_oldest_sort_returns_earliest_first(self, db_session: AsyncSession) -> None:
344 repo = await _make_repo(db_session)
345 p1 = await create_proposal(db_session, repo.repo_id, state="open")
346 p2 = await create_proposal(db_session, repo.repo_id, state="open")
347 p2.created_at = datetime(2030, 1, 2, tzinfo=timezone.utc)
348 p1.created_at = datetime(2030, 1, 1, tzinfo=timezone.utc)
349 await db_session.commit()
350
351 result = await musehub_proposals.list_proposals(
352 db_session, repo.repo_id,
353 filters=ProposalListFilters(state="open", sort="oldest"),
354 )
355 assert result.proposals[0].proposal_id == p1.proposal_id
356
357 @pytest.mark.asyncio
358 async def test_risk_desc_returns_highest_risk_first(self, db_session: AsyncSession) -> None:
359 repo = await _make_repo(db_session)
360 p_low = await create_proposal(db_session, repo.repo_id, state="open")
361 p_high = await create_proposal(db_session, repo.repo_id, state="open")
362 p_low.risk_score = 0.2
363 p_high.risk_score = 0.8
364 await db_session.commit()
365
366 result = await musehub_proposals.list_proposals(
367 db_session, repo.repo_id,
368 filters=ProposalListFilters(state="open", sort="risk_desc"),
369 )
370 assert result.proposals[0].proposal_id == p_high.proposal_id
371
372 @pytest.mark.asyncio
373 async def test_merge_ready_first_surfaces_ready_proposals(self, db_session: AsyncSession) -> None:
374 repo = await _make_repo(db_session)
375 p_ready = await create_proposal(db_session, repo.repo_id, state="open")
376 p_not_ready = await create_proposal(db_session, repo.repo_id, state="open")
377 p_ready.breakage_count = 0
378 p_not_ready.breakage_count = 5
379 await db_session.commit()
380 # Give p_ready two approvals
381 await _make_review(db_session, p_ready.proposal_id, "reviewer-a")
382 await _make_review(db_session, p_ready.proposal_id, "reviewer-b")
383
384 result = await musehub_proposals.list_proposals(
385 db_session, repo.repo_id,
386 filters=ProposalListFilters(state="open", sort="merge_ready_first"),
387 )
388 assert len(result.proposals) == 2
389 assert result.proposals[0].proposal_id == p_ready.proposal_id
390
391
392 class TestIntegrationListProposalsPagination:
393 """list_proposals pagination respects limit and cursor."""
394
395 @pytest.mark.asyncio
396 async def test_limit_caps_result_count(self, db_session: AsyncSession) -> None:
397 repo = await _make_repo(db_session)
398 for _ in range(5):
399 await create_proposal(db_session, repo.repo_id, state="open")
400
401 result = await musehub_proposals.list_proposals(
402 db_session, repo.repo_id,
403 filters=ProposalListFilters(state="open", limit=3),
404 )
405 assert len(result.proposals) == 3
406 assert result.total == 5
407 assert result.next_cursor is not None
408
409 @pytest.mark.asyncio
410 async def test_cursor_advances_to_next_page(self, db_session: AsyncSession) -> None:
411 repo = await _make_repo(db_session)
412 for _ in range(4):
413 await create_proposal(db_session, repo.repo_id, state="open")
414
415 page1 = await musehub_proposals.list_proposals(
416 db_session, repo.repo_id,
417 filters=ProposalListFilters(state="open", sort="oldest", limit=2),
418 )
419 assert page1.next_cursor is not None
420
421 page2 = await musehub_proposals.list_proposals(
422 db_session, repo.repo_id,
423 filters=ProposalListFilters(state="open", sort="oldest", limit=2, cursor=page1.next_cursor),
424 )
425 ids1 = {p.proposal_id for p in page1.proposals}
426 ids2 = {p.proposal_id for p in page2.proposals}
427 assert ids1 & ids2 == set() # no overlap between pages
428
429
430 # ─────────────────────────────────────────────────────────────────────────────
431 # Tier 2 β€” Integration: get_domain_heat
432 # ─────────────────────────────────────────────────────────────────────────────
433
434 class TestIntegrationDomainHeat:
435 """get_domain_heat aggregates domain activity from live rows."""
436
437 @pytest.mark.asyncio
438 async def test_code_domain_count_matches_proposals_with_nonzero_risk(
439 self, db_session: AsyncSession
440 ) -> None:
441 repo = await _make_repo(db_session)
442 p1 = await create_proposal(db_session, repo.repo_id, state="open")
443 p2 = await create_proposal(db_session, repo.repo_id, state="open")
444 p3 = await create_proposal(db_session, repo.repo_id, state="open")
445 p1.risk_score = 0.6
446 p2.risk_score = 0.3
447 p3.risk_score = 0.0
448 await db_session.commit()
449
450 heat = await musehub_proposals.get_domain_heat(repo.repo_id, "open", db_session)
451 assert heat.total_open == 3
452 assert "code" in heat.domains
453 # code is the only domain; all proposals belong to it regardless of risk score
454 assert heat.domains["code"].count == 3
455
456 @pytest.mark.asyncio
457 async def test_empty_repo_returns_zero_counts(self, db_session: AsyncSession) -> None:
458 repo = await _make_repo(db_session)
459 heat = await musehub_proposals.get_domain_heat(repo.repo_id, "open", db_session)
460 assert heat.total_open == 0
461 assert heat.domains == {} or all(v.count == 0 for v in heat.domains.values())
462
463 @pytest.mark.asyncio
464 async def test_merged_proposals_excluded_from_open_heat(
465 self, db_session: AsyncSession
466 ) -> None:
467 repo = await _make_repo(db_session)
468 p_open = await create_proposal(db_session, repo.repo_id, state="open")
469 p_merged = await create_proposal(db_session, repo.repo_id, state="merged")
470 p_open.risk_score = 0.5
471 p_merged.risk_score = 0.9
472 await db_session.commit()
473
474 heat = await musehub_proposals.get_domain_heat(repo.repo_id, "open", db_session)
475 assert heat.total_open == 1
476
477 @pytest.mark.asyncio
478 async def test_avg_risk_in_code_domain(self, db_session: AsyncSession) -> None:
479 repo = await _make_repo(db_session)
480 p1 = await create_proposal(db_session, repo.repo_id, state="open")
481 p2 = await create_proposal(db_session, repo.repo_id, state="open")
482 p1.risk_score = 0.4
483 p2.risk_score = 0.6
484 await db_session.commit()
485
486 heat = await musehub_proposals.get_domain_heat(repo.repo_id, "open", db_session)
487 assert "code" in heat.domains
488 assert abs(heat.domains["code"].avg_risk - 0.5) < 0.01
489
490
491 # ─────────────────────────────────────────────────────────────────────────────
492 # Tier 2 β€” Integration: get_merge_readiness
493 # ─────────────────────────────────────────────────────────────────────────────
494
495 class TestIntegrationMergeReadiness:
496 """get_merge_readiness buckets proposals by merge readiness from live rows."""
497
498 @pytest.mark.asyncio
499 async def test_ready_proposal_with_two_approvals_no_breakage(
500 self, db_session: AsyncSession
501 ) -> None:
502 repo = await _make_repo(db_session)
503 p = await create_proposal(db_session, repo.repo_id, state="open")
504 p.breakage_count = 0
505 await db_session.commit()
506 await _make_review(db_session, p.proposal_id, "r1")
507 await _make_review(db_session, p.proposal_id, "r2")
508
509 readiness = await musehub_proposals.get_merge_readiness(repo.repo_id, db_session)
510 assert p.proposal_number in readiness.ready
511
512 @pytest.mark.asyncio
513 async def test_proposal_with_breakage_goes_to_needs_review(
514 self, db_session: AsyncSession
515 ) -> None:
516 repo = await _make_repo(db_session)
517 p = await create_proposal(db_session, repo.repo_id, state="open")
518 p.breakage_count = 3
519 await db_session.commit()
520 await _make_review(db_session, p.proposal_id, "r1")
521 await _make_review(db_session, p.proposal_id, "r2")
522
523 readiness = await musehub_proposals.get_merge_readiness(repo.repo_id, db_session)
524 assert p.proposal_number in readiness.needs_review
525 assert p.proposal_number not in readiness.ready
526
527 @pytest.mark.asyncio
528 async def test_settling_proposal_goes_to_settling_bucket(
529 self, db_session: AsyncSession
530 ) -> None:
531 repo = await _make_repo(db_session)
532 p = await create_proposal(db_session, repo.repo_id, state="settling")
533 await db_session.commit()
534
535 readiness = await musehub_proposals.get_merge_readiness(repo.repo_id, db_session)
536 assert p.proposal_number in readiness.settling
537
538 @pytest.mark.asyncio
539 async def test_merged_proposals_excluded_from_readiness(
540 self, db_session: AsyncSession
541 ) -> None:
542 repo = await _make_repo(db_session)
543 p_merged = await create_proposal(db_session, repo.repo_id, state="merged")
544 await db_session.commit()
545
546 readiness = await musehub_proposals.get_merge_readiness(repo.repo_id, db_session)
547 all_numbers = readiness.ready + readiness.blocked + readiness.settling + readiness.needs_review
548 assert p_merged.proposal_number not in all_numbers
549
550 @pytest.mark.asyncio
551 async def test_proposal_with_one_approval_goes_to_needs_review(
552 self, db_session: AsyncSession
553 ) -> None:
554 repo = await _make_repo(db_session)
555 p = await create_proposal(db_session, repo.repo_id, state="open")
556 p.breakage_count = 0
557 await db_session.commit()
558 await _make_review(db_session, p.proposal_id, "r1") # only 1, need 2
559
560 readiness = await musehub_proposals.get_merge_readiness(repo.repo_id, db_session)
561 assert p.proposal_number in readiness.needs_review
562 assert p.proposal_number not in readiness.ready
563
564
565 # ─────────────────────────────────────────────────────────────────────────────
566 # Tier 6 β€” Performance: query timing assertions
567 # ─────────────────────────────────────────────────────────────────────────────
568
569 class TestPerformanceListProposals:
570 """list_proposals completes within documented time budgets.
571
572 Tier 6 (Performance): measured assertions on the test DB.
573 Thresholds are deliberately generous to avoid flaky CI β€” these are floor
574 benchmarks, not tight SLAs.
575 """
576
577 @pytest.mark.asyncio
578 async def test_list_20_proposals_under_100ms(self, db_session: AsyncSession) -> None:
579 repo = await _make_repo(db_session)
580 for _ in range(20):
581 await create_proposal(db_session, repo.repo_id, state="open")
582
583 t0 = time.perf_counter()
584 result = await musehub_proposals.list_proposals(
585 db_session, repo.repo_id, filters=ProposalListFilters(state="open", limit=20)
586 )
587 elapsed_ms = (time.perf_counter() - t0) * 1000
588 assert len(result.proposals) == 20
589 assert elapsed_ms < 100, f"list_proposals took {elapsed_ms:.1f}ms (budget: 100ms)"
590
591 @pytest.mark.asyncio
592 async def test_get_domain_heat_under_50ms(self, db_session: AsyncSession) -> None:
593 repo = await _make_repo(db_session)
594 for i in range(10):
595 p = await create_proposal(db_session, repo.repo_id, state="open")
596 p.risk_score = float(i) / 10
597 await db_session.commit()
598
599 t0 = time.perf_counter()
600 heat = await musehub_proposals.get_domain_heat(repo.repo_id, "open", db_session)
601 elapsed_ms = (time.perf_counter() - t0) * 1000
602 assert heat.total_open == 10
603 assert elapsed_ms < 50, f"get_domain_heat took {elapsed_ms:.1f}ms (budget: 50ms)"
604
605 @pytest.mark.asyncio
606 async def test_get_merge_readiness_under_50ms(self, db_session: AsyncSession) -> None:
607 repo = await _make_repo(db_session)
608 for _ in range(10):
609 await create_proposal(db_session, repo.repo_id, state="open")
610
611 t0 = time.perf_counter()
612 readiness = await musehub_proposals.get_merge_readiness(repo.repo_id, db_session)
613 elapsed_ms = (time.perf_counter() - t0) * 1000
614 assert len(readiness.needs_review) == 10
615 assert elapsed_ms < 50, f"get_merge_readiness took {elapsed_ms:.1f}ms (budget: 50ms)"
616
617 @pytest.mark.asyncio
618 async def test_risk_desc_sort_under_100ms(self, db_session: AsyncSession) -> None:
619 repo = await _make_repo(db_session)
620 for i in range(20):
621 p = await create_proposal(db_session, repo.repo_id, state="open")
622 p.risk_score = float(i) / 20
623 await db_session.commit()
624
625 t0 = time.perf_counter()
626 result = await musehub_proposals.list_proposals(
627 db_session, repo.repo_id,
628 filters=ProposalListFilters(state="open", sort="risk_desc", limit=20),
629 )
630 elapsed_ms = (time.perf_counter() - t0) * 1000
631 assert len(result.proposals) == 20
632 assert elapsed_ms < 100, f"risk_desc sort took {elapsed_ms:.1f}ms (budget: 100ms)"