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