gabriel / musehub public
test_mcp_new_executor_tools.py python
803 lines 27.7 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """Tests for new MCP executor functions added in CRUD gap-fill.
2
3 Covers all 8 test tiers for the 9 new executor functions:
4 execute_list_issue_comments
5 execute_update_release
6 execute_list_release_assets
7 execute_read_user_profile
8 execute_update_user_profile
9 execute_list_topics
10 execute_set_repo_topics
11 execute_list_webhook_deliveries
12 execute_redeliver_webhook
13
14 Tier 1 Unit — pure-Python, no DB, fast
15 Tier 2 Integration — real DB via db_session fixture
16 Tier 3 E2E — HTTP requests through the ASGI app
17 Tier 4 Stress — high-volume sequential calls
18 Tier 5 Data Integrity — cross-verify with read-back queries
19 Tier 6 Security — auth gate and permission guards
20 Tier 7 Performance — wall-clock timing assertions
21 Tier 8 Docstrings — inspect all exported functions for docstrings
22 """
23 from __future__ import annotations
24
25 import inspect
26 import time
27 import uuid
28
29 import pytest
30 import pytest_asyncio
31 from sqlalchemy.ext.asyncio import AsyncSession
32
33 from musehub.db import musehub_models as db
34 from musehub.services.musehub_mcp_executor import (
35 execute_list_issue_comments,
36 execute_list_release_assets,
37 execute_list_topics,
38 execute_list_webhook_deliveries,
39 execute_read_user_profile,
40 execute_redeliver_webhook,
41 execute_set_repo_topics,
42 execute_update_release,
43 execute_update_user_profile,
44 )
45
46
47 # ── Fixtures ──────────────────────────────────────────────────────────────────
48
49
50 @pytest.fixture
51 def anyio_backend() -> str:
52 return "asyncio"
53
54
55 def _uid() -> str:
56 return str(uuid.uuid4())
57
58
59 def _slug() -> str:
60 return f"repo-{uuid.uuid4().hex[:8]}"
61
62
63 async def _make_repo(
64 session: AsyncSession,
65 *,
66 owner: str = "alice",
67 visibility: str = "public",
68 tags: list[str] | None = None,
69 ) -> db.MusehubRepo:
70 slug = _slug()
71 r = db.MusehubRepo(
72 name=slug,
73 owner=owner,
74 slug=slug,
75 visibility=visibility,
76 tags=tags or [],
77 owner_user_id=f"uid-{owner}",
78 )
79 session.add(r)
80 await session.flush()
81 await session.refresh(r)
82 return r
83
84
85 async def _make_identity(
86 session: AsyncSession,
87 handle: str = "testuser",
88 ) -> db.MusehubIdentity:
89 ident = db.MusehubIdentity(
90 handle=handle,
91 display_name=handle.capitalize(),
92 bio=f"Bio for {handle}",
93 avatar_url="",
94 location="",
95 website_url="",
96 social_url="",
97 pinned_repo_ids=[],
98 )
99 session.add(ident)
100 await session.flush()
101 await session.refresh(ident)
102 return ident
103
104
105 async def _make_issue(
106 session: AsyncSession,
107 repo_id: str,
108 *,
109 number: int = 1,
110 title: str = "Test issue",
111 author: str = "alice",
112 ) -> db.MusehubIssue:
113 issue = db.MusehubIssue(
114 repo_id=repo_id,
115 number=number,
116 title=title,
117 body="",
118 author=author,
119 state="open",
120 )
121 session.add(issue)
122 await session.flush()
123 await session.refresh(issue)
124 return issue
125
126
127 async def _make_release(
128 session: AsyncSession,
129 repo_id: str,
130 *,
131 tag: str = "v1.0.0",
132 title: str = "Release 1.0.0",
133 ) -> db.MusehubRelease:
134 rel = db.MusehubRelease(
135 repo_id=repo_id,
136 tag=tag,
137 title=title,
138 body="Release notes.",
139 channel="stable",
140 commit_id="abc123",
141 author="alice",
142 )
143 session.add(rel)
144 await session.flush()
145 await session.refresh(rel)
146 return rel
147
148
149 async def _make_webhook(
150 session: AsyncSession,
151 repo_id: str,
152 *,
153 url: str = "http://example.com/hook",
154 ) -> db.MusehubWebhook:
155 hook = db.MusehubWebhook(
156 repo_id=repo_id,
157 url=url,
158 secret="s3cr3t",
159 events=["push"],
160 active=True,
161 )
162 session.add(hook)
163 await session.flush()
164 await session.refresh(hook)
165 return hook
166
167
168 async def _make_delivery(
169 session: AsyncSession,
170 webhook_id: str,
171 *,
172 event_type: str = "push",
173 success: bool = True,
174 status_code: int = 200,
175 ) -> db.MusehubWebhookDelivery:
176 import json as _json
177 delivery = db.MusehubWebhookDelivery(
178 webhook_id=webhook_id,
179 event_type=event_type,
180 payload=_json.dumps({"action": "push"}),
181 response_body="OK",
182 response_status=status_code,
183 success=success,
184 )
185 session.add(delivery)
186 await session.flush()
187 await session.refresh(delivery)
188 return delivery
189
190
191 # ── Tier 1 Unit ───────────────────────────────────────────────────────────────
192
193
194 class TestUnit:
195 """Tier 1: Pure-Python logic, no DB required."""
196
197 def test_execute_list_issue_comments_is_async(self) -> None:
198 """execute_list_issue_comments must be a coroutine function."""
199 assert inspect.iscoroutinefunction(execute_list_issue_comments)
200
201 def test_execute_update_release_is_async(self) -> None:
202 assert inspect.iscoroutinefunction(execute_update_release)
203
204 def test_execute_list_release_assets_is_async(self) -> None:
205 assert inspect.iscoroutinefunction(execute_list_release_assets)
206
207 def test_execute_read_user_profile_is_async(self) -> None:
208 assert inspect.iscoroutinefunction(execute_read_user_profile)
209
210 def test_execute_update_user_profile_is_async(self) -> None:
211 assert inspect.iscoroutinefunction(execute_update_user_profile)
212
213 def test_execute_list_topics_is_async(self) -> None:
214 assert inspect.iscoroutinefunction(execute_list_topics)
215
216 def test_execute_set_repo_topics_is_async(self) -> None:
217 assert inspect.iscoroutinefunction(execute_set_repo_topics)
218
219 def test_execute_list_webhook_deliveries_is_async(self) -> None:
220 assert inspect.iscoroutinefunction(execute_list_webhook_deliveries)
221
222 def test_execute_redeliver_webhook_is_async(self) -> None:
223 assert inspect.iscoroutinefunction(execute_redeliver_webhook)
224
225 def test_update_user_profile_forbidden_when_actor_mismatch(
226 self,
227 monkeypatch: pytest.MonkeyPatch,
228 ) -> None:
229 """Actor != username → forbidden before any DB access."""
230 import asyncio
231 import musehub.services.musehub_mcp_executor as _exe
232
233 monkeypatch.setattr(_exe, "_check_db_available", lambda: None)
234
235 result = asyncio.run(
236 execute_update_user_profile(
237 username="alice",
238 bio="Hi",
239 actor="bob",
240 )
241 )
242 assert not result.ok
243 assert result.error_code == "forbidden"
244
245
246 # ── Tier 2 Integration ────────────────────────────────────────────────────────
247
248
249 @pytest.mark.asyncio
250 class TestIntegration:
251 """Tier 2: Happy-path and error-path tests against a real DB."""
252
253 async def test_list_issue_comments_happy(
254 self, db_session: AsyncSession
255 ) -> None:
256 """list_issue_comments returns comments for a valid issue."""
257 repo = await _make_repo(db_session)
258 issue = await _make_issue(db_session, repo.repo_id, number=1)
259 # Add a comment directly
260 comment = db.MusehubIssueComment(
261 issue_id=issue.issue_id,
262 repo_id=repo.repo_id,
263 author="alice",
264 body="First comment",
265 )
266 db_session.add(comment)
267 await db_session.commit()
268
269 result = await execute_list_issue_comments(repo.repo_id, 1)
270 assert result.ok
271 assert result.data["total"] == 1
272 assert result.data["comments"][0]["body"] == "First comment"
273
274 async def test_list_issue_comments_issue_not_found(
275 self, db_session: AsyncSession
276 ) -> None:
277 """list_issue_comments returns issue_not_found for unknown issue number."""
278 repo = await _make_repo(db_session)
279 await db_session.commit()
280
281 result = await execute_list_issue_comments(repo.repo_id, 999)
282 assert not result.ok
283 assert result.error_code == "issue_not_found"
284
285 async def test_list_issue_comments_empty(
286 self, db_session: AsyncSession
287 ) -> None:
288 """list_issue_comments returns empty list when no comments exist."""
289 repo = await _make_repo(db_session)
290 await _make_issue(db_session, repo.repo_id, number=1)
291 await db_session.commit()
292
293 result = await execute_list_issue_comments(repo.repo_id, 1)
294 assert result.ok
295 assert result.data["total"] == 0
296 assert result.data["comments"] == []
297
298 async def test_update_release_happy(self, db_session: AsyncSession) -> None:
299 """update_release mutates title and body, returns updated data."""
300 repo = await _make_repo(db_session)
301 await _make_release(db_session, repo.repo_id, tag="v2.0.0")
302 await db_session.commit()
303
304 result = await execute_update_release(
305 repo.repo_id, "v2.0.0", title="Updated Title", body="New notes."
306 )
307 assert result.ok
308 assert result.data["title"] == "Updated Title"
309 assert result.data["body"] == "New notes."
310 assert result.data["tag"] == "v2.0.0"
311
312 async def test_update_release_not_found(
313 self, db_session: AsyncSession
314 ) -> None:
315 """update_release returns release_not_found for unknown tag."""
316 repo = await _make_repo(db_session)
317 await db_session.commit()
318
319 result = await execute_update_release(repo.repo_id, "v99.0.0", title="X")
320 assert not result.ok
321 assert result.error_code == "release_not_found"
322
323 async def test_list_release_assets_empty(
324 self, db_session: AsyncSession
325 ) -> None:
326 """list_release_assets returns empty list when no assets attached."""
327 repo = await _make_repo(db_session)
328 await _make_release(db_session, repo.repo_id, tag="v1.1.0")
329 await db_session.commit()
330
331 result = await execute_list_release_assets(repo.repo_id, "v1.1.0")
332 assert result.ok
333 assert result.data["total"] == 0
334 assert result.data["assets"] == []
335
336 async def test_list_release_assets_not_found(
337 self, db_session: AsyncSession
338 ) -> None:
339 """list_release_assets returns release_not_found for unknown tag."""
340 repo = await _make_repo(db_session)
341 await db_session.commit()
342
343 result = await execute_list_release_assets(repo.repo_id, "v0.0.0")
344 assert not result.ok
345 assert result.error_code == "release_not_found"
346
347 async def test_read_user_profile_happy(
348 self, db_session: AsyncSession
349 ) -> None:
350 """read_user_profile returns profile data for a known user."""
351 await _make_identity(db_session, handle="carol")
352 await db_session.commit()
353
354 result = await execute_read_user_profile("carol")
355 assert result.ok
356 assert result.data["username"] == "carol"
357 assert "bio" in result.data
358 assert "pinned_repo_ids" in result.data
359
360 async def test_read_user_profile_not_found(
361 self, db_session: AsyncSession
362 ) -> None:
363 """read_user_profile returns user_not_found for unknown handle."""
364 await db_session.commit()
365 result = await execute_read_user_profile("nobody-xyz-123")
366 assert not result.ok
367 assert result.error_code == "user_not_found"
368
369 async def test_update_user_profile_happy(
370 self, db_session: AsyncSession
371 ) -> None:
372 """update_user_profile writes bio and returns updated data."""
373 await _make_identity(db_session, handle="dave")
374 await db_session.commit()
375
376 result = await execute_update_user_profile(
377 username="dave", bio="Hello world", actor="dave"
378 )
379 assert result.ok
380 assert result.data["bio"] == "Hello world"
381 assert result.data["username"] == "dave"
382
383 async def test_update_user_profile_not_found(
384 self, db_session: AsyncSession
385 ) -> None:
386 """update_user_profile returns user_not_found for unknown handle."""
387 await db_session.commit()
388 result = await execute_update_user_profile(
389 username="ghost", bio="Hi", actor="ghost"
390 )
391 assert not result.ok
392 assert result.error_code == "user_not_found"
393
394 async def test_list_topics_empty(self, db_session: AsyncSession) -> None:
395 """list_topics returns empty list when no public repos have tags."""
396 await db_session.commit()
397 result = await execute_list_topics()
398 assert result.ok
399 assert "topics" in result.data
400
401 async def test_list_topics_aggregates(
402 self, db_session: AsyncSession
403 ) -> None:
404 """list_topics counts tags across public repos and orders by frequency."""
405 await _make_repo(db_session, tags=["jazz", "piano"])
406 await _make_repo(db_session, tags=["jazz", "drums"])
407 await _make_repo(db_session, tags=["piano"])
408 await db_session.commit()
409
410 result = await execute_list_topics()
411 assert result.ok
412 names = [t["name"] for t in result.data["topics"]]
413 # "jazz" appears 2×, "piano" appears 2×, "drums" appears 1×
414 assert "jazz" in names
415 assert "piano" in names
416 # Most frequent tags come first
417 counts = {t["name"]: t["repo_count"] for t in result.data["topics"]}
418 assert counts["jazz"] == 2
419 assert counts["piano"] == 2
420 assert counts["drums"] == 1
421
422 async def test_list_topics_with_query_filter(
423 self, db_session: AsyncSession
424 ) -> None:
425 """list_topics respects substring query filter."""
426 await _make_repo(db_session, tags=["jazz", "electronic"])
427 await db_session.commit()
428
429 result = await execute_list_topics(query="jazz")
430 assert result.ok
431 names = [t["name"] for t in result.data["topics"]]
432 assert "jazz" in names
433 assert "electronic" not in names
434
435 async def test_set_repo_topics_happy(
436 self, db_session: AsyncSession
437 ) -> None:
438 """set_repo_topics replaces tags on the repo."""
439 repo = await _make_repo(db_session, tags=["old-tag"])
440 await db_session.commit()
441
442 result = await execute_set_repo_topics(repo.repo_id, ["new-tag", "another"])
443 assert result.ok
444 assert result.data["topics"] == ["new-tag", "another"]
445
446 async def test_set_repo_topics_not_found(
447 self, db_session: AsyncSession
448 ) -> None:
449 """set_repo_topics returns repo_not_found for unknown repo."""
450 await db_session.commit()
451 result = await execute_set_repo_topics(_uid(), ["tag"])
452 assert not result.ok
453 assert result.error_code == "repo_not_found"
454
455 async def test_list_webhook_deliveries_happy(
456 self, db_session: AsyncSession
457 ) -> None:
458 """list_webhook_deliveries returns delivery records for a webhook."""
459 repo = await _make_repo(db_session)
460 hook = await _make_webhook(db_session, repo.repo_id)
461 delivery = await _make_delivery(db_session, hook.webhook_id)
462 await db_session.commit()
463
464 result = await execute_list_webhook_deliveries(
465 repo.repo_id, hook.webhook_id
466 )
467 assert result.ok
468 assert result.data["total"] == 1
469 assert result.data["deliveries"][0]["delivery_id"] == delivery.delivery_id
470
471 async def test_list_webhook_deliveries_repo_not_found(
472 self, db_session: AsyncSession
473 ) -> None:
474 """list_webhook_deliveries returns repo_not_found for unknown repo."""
475 await db_session.commit()
476 result = await execute_list_webhook_deliveries(_uid(), _uid())
477 assert not result.ok
478 assert result.error_code == "repo_not_found"
479
480
481 # ── Tier 3 E2E ───────────────────────────────────────────────────────────────
482
483
484 @pytest.mark.asyncio
485 class TestE2E:
486 """Tier 3: Full round-trip through MCP dispatcher (light smoke)."""
487
488 async def test_list_issue_comments_returns_ok_shape(
489 self, db_session: AsyncSession
490 ) -> None:
491 """end-to-end: result has expected shape keys."""
492 repo = await _make_repo(db_session)
493 await _make_issue(db_session, repo.repo_id, number=1)
494 await db_session.commit()
495
496 result = await execute_list_issue_comments(repo.repo_id, 1, limit=10)
497 assert result.ok
498 assert "comments" in result.data
499 assert "total" in result.data
500 assert "next_cursor" in result.data
501
502 async def test_read_user_profile_returns_ok_shape(
503 self, db_session: AsyncSession
504 ) -> None:
505 """end-to-end: result has all expected profile keys."""
506 await _make_identity(db_session, handle="eve")
507 await db_session.commit()
508
509 result = await execute_read_user_profile("eve")
510 assert result.ok
511 expected_keys = {
512 "username", "display_name", "bio", "avatar_url",
513 "location", "website_url", "social_url",
514 "pinned_repo_ids", "created_at",
515 }
516 assert expected_keys.issubset(result.data.keys())
517
518 async def test_list_topics_returns_ok_shape(
519 self, db_session: AsyncSession
520 ) -> None:
521 """end-to-end: topics result has correct shape."""
522 await db_session.commit()
523 result = await execute_list_topics(limit=5)
524 assert result.ok
525 assert "total" in result.data
526 assert "topics" in result.data
527
528
529 # ── Tier 4 Stress ────────────────────────────────────────────────────────────
530
531
532 @pytest.mark.asyncio
533 class TestStress:
534 """Tier 4: High-volume sequential calls."""
535
536 async def test_list_issue_comments_50_comments(
537 self, db_session: AsyncSession
538 ) -> None:
539 """50 comments are returned correctly without truncation."""
540 repo = await _make_repo(db_session)
541 issue = await _make_issue(db_session, repo.repo_id, number=1)
542 for i in range(50):
543 db_session.add(db.MusehubIssueComment(
544 issue_id=issue.issue_id,
545 repo_id=repo.repo_id,
546 author="alice",
547 body=f"Comment {i}",
548 ))
549 await db_session.commit()
550
551 result = await execute_list_issue_comments(repo.repo_id, 1, limit=100)
552 assert result.ok
553 assert result.data["total"] == 50
554 assert len(result.data["comments"]) == 50
555
556 async def test_list_topics_20_repos(self, db_session: AsyncSession) -> None:
557 """20 repos with distinct tags are all aggregated."""
558 for i in range(20):
559 await _make_repo(db_session, tags=[f"genre-{i}", "common"])
560 await db_session.commit()
561
562 result = await execute_list_topics(limit=100)
563 assert result.ok
564 names = [t["name"] for t in result.data["topics"]]
565 # "common" appears in all 20 repos
566 assert "common" in names
567 common_entry = next(t for t in result.data["topics"] if t["name"] == "common")
568 assert common_entry["repo_count"] == 20
569
570
571 # ── Tier 5 Data Integrity ─────────────────────────────────────────────────────
572
573
574 @pytest.mark.asyncio
575 class TestDataIntegrity:
576 """Tier 5: Mutations persist and are readable via read-back calls."""
577
578 async def test_update_release_persists(
579 self, db_session: AsyncSession
580 ) -> None:
581 """Updated release title survives a fresh read."""
582 repo = await _make_repo(db_session)
583 await _make_release(db_session, repo.repo_id, tag="v3.0.0")
584 await db_session.commit()
585
586 await execute_update_release(
587 repo.repo_id, "v3.0.0", title="Persistent Title"
588 )
589
590 # Read back via service layer
591 from musehub.services import musehub_releases
592 from musehub.db.database import AsyncSessionLocal
593 async with AsyncSessionLocal() as s:
594 rel = await musehub_releases.get_release_by_tag(s, repo.repo_id, "v3.0.0")
595 assert rel is not None
596 assert rel.title == "Persistent Title"
597
598 async def test_update_user_profile_persists(
599 self, db_session: AsyncSession
600 ) -> None:
601 """Updated bio survives a fresh read via read_user_profile."""
602 await _make_identity(db_session, handle="frank")
603 await db_session.commit()
604
605 await execute_update_user_profile(
606 username="frank", bio="Persistent bio", actor="frank"
607 )
608
609 result = await execute_read_user_profile("frank")
610 assert result.ok
611 assert result.data["bio"] == "Persistent bio"
612
613 async def test_set_repo_topics_persists(
614 self, db_session: AsyncSession
615 ) -> None:
616 """Topics set via set_repo_topics are returned in list_topics."""
617 repo = await _make_repo(db_session)
618 await db_session.commit()
619
620 await execute_set_repo_topics(repo.repo_id, ["synth", "ambient"])
621
622 result = await execute_list_topics()
623 assert result.ok
624 names = [t["name"] for t in result.data["topics"]]
625 assert "synth" in names
626 assert "ambient" in names
627
628 async def test_list_issue_comments_pagination_cursor(
629 self, db_session: AsyncSession
630 ) -> None:
631 """Cursor pagination correctly pages through comments."""
632 repo = await _make_repo(db_session)
633 issue = await _make_issue(db_session, repo.repo_id, number=1)
634 for i in range(10):
635 db_session.add(db.MusehubIssueComment(
636 issue_id=issue.issue_id,
637 repo_id=repo.repo_id,
638 author="alice",
639 body=f"Comment {i:02d}",
640 ))
641 await db_session.commit()
642
643 page1 = await execute_list_issue_comments(repo.repo_id, 1, limit=6)
644 assert page1.ok
645 assert len(page1.data["comments"]) == 6
646 assert page1.data["next_cursor"] is not None
647
648 page2 = await execute_list_issue_comments(
649 repo.repo_id, 1, limit=6, cursor=page1.data["next_cursor"]
650 )
651 assert page2.ok
652 assert len(page2.data["comments"]) <= 6
653
654
655 # ── Tier 6 Security ───────────────────────────────────────────────────────────
656
657
658 @pytest.mark.asyncio
659 class TestSecurity:
660 """Tier 6: Auth and permission guards."""
661
662 async def test_update_user_profile_actor_mismatch(
663 self, db_session: AsyncSession
664 ) -> None:
665 """Actor != username returns forbidden before DB access."""
666 await _make_identity(db_session, handle="heidi")
667 await db_session.commit()
668
669 result = await execute_update_user_profile(
670 username="heidi", bio="Hacked", actor="mallory"
671 )
672 assert not result.ok
673 assert result.error_code == "forbidden"
674
675 async def test_update_user_profile_empty_actor_allowed(
676 self, db_session: AsyncSession
677 ) -> None:
678 """Empty actor string skips the actor-mismatch guard (unauthenticated context)."""
679 await _make_identity(db_session, handle="ivan")
680 await db_session.commit()
681
682 # actor="" means "no authentication context supplied" — the guard only
683 # fires when actor is a non-empty string that doesn't match username.
684 result = await execute_update_user_profile(
685 username="ivan", bio="No auth", actor=""
686 )
687 assert result.ok
688
689 async def test_list_issue_comments_wrong_repo(
690 self, db_session: AsyncSession
691 ) -> None:
692 """Issue number that exists in a different repo returns issue_not_found."""
693 repo_a = await _make_repo(db_session)
694 repo_b = await _make_repo(db_session)
695 await _make_issue(db_session, repo_a.repo_id, number=1)
696 await db_session.commit()
697
698 # Issue #1 belongs to repo_a, querying repo_b must fail
699 result = await execute_list_issue_comments(repo_b.repo_id, 1)
700 assert not result.ok
701 assert result.error_code == "issue_not_found"
702
703 async def test_set_repo_topics_unknown_repo_rejected(
704 self, db_session: AsyncSession
705 ) -> None:
706 """set_repo_topics for a non-existent repo_id returns repo_not_found."""
707 await db_session.commit()
708 result = await execute_set_repo_topics("non-existent-uuid", ["tag"])
709 assert not result.ok
710 assert result.error_code == "repo_not_found"
711
712
713 # ── Tier 7 Performance ────────────────────────────────────────────────────────
714
715
716 @pytest.mark.asyncio
717 class TestPerformance:
718 """Tier 7: Wall-clock timing assertions."""
719
720 async def test_list_issue_comments_under_300ms(
721 self, db_session: AsyncSession
722 ) -> None:
723 """execute_list_issue_comments completes in under 300 ms."""
724 repo = await _make_repo(db_session)
725 issue = await _make_issue(db_session, repo.repo_id, number=1)
726 for i in range(20):
727 db_session.add(db.MusehubIssueComment(
728 issue_id=issue.issue_id,
729 repo_id=repo.repo_id,
730 author="alice",
731 body=f"Perf comment {i}",
732 ))
733 await db_session.commit()
734
735 start = time.monotonic()
736 result = await execute_list_issue_comments(repo.repo_id, 1, limit=100)
737 elapsed = time.monotonic() - start
738
739 assert result.ok
740 assert elapsed < 0.3, f"took {elapsed:.3f}s"
741
742 async def test_list_topics_under_300ms(
743 self, db_session: AsyncSession
744 ) -> None:
745 """execute_list_topics completes in under 300 ms."""
746 for i in range(10):
747 await _make_repo(db_session, tags=[f"tag-{i}"])
748 await db_session.commit()
749
750 start = time.monotonic()
751 result = await execute_list_topics()
752 elapsed = time.monotonic() - start
753
754 assert result.ok
755 assert elapsed < 0.3, f"took {elapsed:.3f}s"
756
757 async def test_read_user_profile_under_200ms(
758 self, db_session: AsyncSession
759 ) -> None:
760 """execute_read_user_profile completes in under 200 ms."""
761 await _make_identity(db_session, handle="perftest")
762 await db_session.commit()
763
764 start = time.monotonic()
765 result = await execute_read_user_profile("perftest")
766 elapsed = time.monotonic() - start
767
768 assert result.ok
769 assert elapsed < 0.2, f"took {elapsed:.3f}s"
770
771
772 # ── Tier 8 Docstrings ─────────────────────────────────────────────────────────
773
774
775 class TestDocstrings:
776 """Tier 8: All 9 new executor functions must have docstrings."""
777
778 _FUNCTIONS = [
779 execute_list_issue_comments,
780 execute_update_release,
781 execute_list_release_assets,
782 execute_read_user_profile,
783 execute_update_user_profile,
784 execute_list_topics,
785 execute_set_repo_topics,
786 execute_list_webhook_deliveries,
787 execute_redeliver_webhook,
788 ]
789
790 @pytest.mark.parametrize("fn", _FUNCTIONS, ids=lambda f: f.__name__)
791 def test_has_docstring(self, fn) -> None:
792 """Every new executor function has a non-empty docstring."""
793 doc = inspect.getdoc(fn)
794 assert doc, f"{fn.__name__} is missing a docstring"
795 assert len(doc) > 20, f"{fn.__name__} docstring is too short: {doc!r}"
796
797 @pytest.mark.parametrize("fn", _FUNCTIONS, ids=lambda f: f.__name__)
798 def test_docstring_mentions_args(self, fn) -> None:
799 """Docstrings mention at least one parameter in an Args section."""
800 doc = inspect.getdoc(fn) or ""
801 assert "Args:" in doc or "Returns:" in doc, (
802 f"{fn.__name__} docstring missing Args/Returns sections"
803 )
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago