gabriel / musehub public
test_sessions_section30.py python
846 lines 31.1 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """Section 30 — Sessions: 7-layer test suite.
2
3 Covers:
4 musehub/services/musehub_sessions.py — _to_response, upsert_session, list_sessions, get_session
5 musehub/services/musehub_repository.py — create_session, stop_session, list_sessions, get_session
6 musehub/api/routes/musehub/repos.py — POST/GET/GET/{id}/POST/{id}/stop endpoints
7 musehub/db/musehub_models.py — MusehubSession ORM model
8 musehub/models/musehub.py — SessionCreate, SessionStop, SessionResponse
9
10 HTTP routes (all under /api):
11 POST /repos/{repo_id}/sessions → create_session (201, auth required)
12 GET /repos/{repo_id}/sessions → list_sessions (200, optional auth)
13 GET /repos/{repo_id}/sessions/{sid} → get_session (200, optional auth)
14 POST /repos/{repo_id}/sessions/{sid}/stop → stop_session (200, auth required)
15 """
16 from __future__ import annotations
17
18 import time
19 import uuid
20 from datetime import datetime, timezone
21
22 import pytest
23 from httpx import AsyncClient
24 from sqlalchemy.ext.asyncio import AsyncSession
25
26 from musehub.db.musehub_models import MusehubRepo, MusehubSession
27 from musehub.models.musehub import SessionCreate, SessionResponse, SessionStop
28 from musehub.services import musehub_repository, musehub_sessions
29 from musehub.muse_contracts.json_types import StrDict
30
31
32 # ── helpers ───────────────────────────────────────────────────────────────────
33
34
35 def _uid() -> str:
36 return str(uuid.uuid4())
37
38
39 def _now() -> datetime:
40 return datetime.now(tz=timezone.utc)
41
42
43 async def _db_repo(session: AsyncSession, *, visibility: str = "public") -> MusehubRepo:
44 slug = f"sess-repo-{_uid()[:8]}"
45 repo = MusehubRepo(
46 repo_id=_uid(),
47 name=slug,
48 slug=slug,
49 owner="testuser",
50 owner_user_id="testuser",
51 visibility=visibility,
52 )
53 session.add(repo)
54 await session.flush()
55 return repo
56
57
58 async def _db_session(
59 session: AsyncSession,
60 repo_id: str,
61 *,
62 is_active: bool = True,
63 participants: list[str] | None = None,
64 ) -> MusehubSession:
65 s = MusehubSession(
66 session_id=_uid(),
67 repo_id=repo_id,
68 started_at=_now(),
69 participants=participants or [],
70 location="Test Studio",
71 intent="test intent",
72 is_active=is_active,
73 )
74 session.add(s)
75 await session.flush()
76 return s
77
78
79 # ═══════════════════════════════════════════════════════════════════════════════
80 # Layer 1 — Unit
81 # ═══════════════════════════════════════════════════════════════════════════════
82
83
84 class TestUnitSessions:
85 """Pure logic tests — no DB, no HTTP."""
86
87 def test_session_create_defaults(self) -> None:
88 sc = SessionCreate()
89 assert sc.participants == []
90 assert sc.intent == ""
91 assert sc.location == ""
92 assert sc.started_at is None
93 assert sc.is_active is True
94
95 def test_session_create_with_data(self) -> None:
96 t = _now()
97 sc = SessionCreate(
98 started_at=t,
99 participants=["alice", "bob"],
100 intent="Write the chorus",
101 location="Abbey Road",
102 )
103 assert sc.participants == ["alice", "bob"]
104 assert sc.started_at == t
105
106 def test_session_stop_defaults(self) -> None:
107 ss = SessionStop()
108 assert ss.ended_at is None
109
110 def test_session_stop_with_time(self) -> None:
111 t = _now()
112 ss = SessionStop(ended_at=t)
113 assert ss.ended_at == t
114
115 def test_session_response_fields(self) -> None:
116 t = _now()
117 sr = SessionResponse(
118 session_id="abc",
119 started_at=t,
120 ended_at=None,
121 duration_seconds=None,
122 participants=["carol"],
123 commits=[],
124 notes="",
125 intent="jam",
126 location="studio",
127 is_active=True,
128 created_at=t,
129 )
130 assert sr.session_id == "abc"
131 assert sr.is_active is True
132 assert sr.duration_seconds is None
133
134 def test_session_response_duration_none_when_active(self) -> None:
135 t = _now()
136 sr = SessionResponse(
137 session_id="x",
138 started_at=t,
139 ended_at=None,
140 duration_seconds=None,
141 participants=[],
142 commits=[],
143 notes="",
144 intent="",
145 location="",
146 is_active=True,
147 created_at=t,
148 )
149 assert sr.duration_seconds is None
150
151 def test_session_response_commits_default_empty(self) -> None:
152 t = _now()
153 sr = SessionResponse(
154 session_id="x",
155 started_at=t,
156 participants=[],
157 commits=[],
158 notes="",
159 intent="",
160 location="",
161 is_active=False,
162 created_at=t,
163 )
164 assert sr.commits == []
165
166
167 # ═══════════════════════════════════════════════════════════════════════════════
168 # Layer 2 — Integration
169 # ═══════════════════════════════════════════════════════════════════════════════
170
171
172 class TestIntegrationSessionService:
173 """Real DB, service-layer calls."""
174
175 @pytest.mark.anyio
176 async def test_create_session_returns_response(self, db_session: AsyncSession) -> None:
177 repo = await _db_repo(db_session)
178 await db_session.commit()
179
180 resp = await musehub_repository.create_session(
181 db_session,
182 repo.repo_id,
183 started_at=None,
184 participants=["alice"],
185 intent="write",
186 location="studio",
187 )
188 assert isinstance(resp, SessionResponse)
189 assert resp.is_active is True
190 assert resp.session_id is not None
191
192 @pytest.mark.anyio
193 async def test_create_session_uses_provided_started_at(self, db_session: AsyncSession) -> None:
194 repo = await _db_repo(db_session)
195 await db_session.commit()
196 t = _now()
197
198 resp = await musehub_repository.create_session(
199 db_session, repo.repo_id, started_at=t,
200 participants=[], intent="", location="",
201 )
202 # started_at stored as UTC; compare without tz
203 assert resp.started_at.replace(tzinfo=None) == t.replace(tzinfo=None)
204
205 @pytest.mark.anyio
206 async def test_list_sessions_empty(self, db_session: AsyncSession) -> None:
207 repo = await _db_repo(db_session)
208 await db_session.commit()
209
210 sessions, total = await musehub_repository.list_sessions(db_session, repo.repo_id)
211 assert sessions == []
212 assert total == 0
213
214 @pytest.mark.anyio
215 async def test_list_sessions_returns_all(self, db_session: AsyncSession) -> None:
216 repo = await _db_repo(db_session)
217 await _db_session(db_session, repo.repo_id)
218 await _db_session(db_session, repo.repo_id)
219 await db_session.commit()
220
221 sessions, total = await musehub_repository.list_sessions(db_session, repo.repo_id)
222 assert total == 2
223 assert len(sessions) == 2
224
225 @pytest.mark.anyio
226 async def test_list_sessions_limit(self, db_session: AsyncSession) -> None:
227 repo = await _db_repo(db_session)
228 for _ in range(5):
229 await _db_session(db_session, repo.repo_id)
230 await db_session.commit()
231
232 sessions, total = await musehub_repository.list_sessions(db_session, repo.repo_id, limit=3)
233 assert total == 5
234 assert len(sessions) == 3
235
236 @pytest.mark.anyio
237 async def test_get_session_found(self, db_session: AsyncSession) -> None:
238 repo = await _db_repo(db_session)
239 sess = await _db_session(db_session, repo.repo_id)
240 await db_session.commit()
241
242 result = await musehub_repository.get_session(db_session, repo.repo_id, sess.session_id)
243 assert result is not None
244 assert result.session_id == sess.session_id
245
246 @pytest.mark.anyio
247 async def test_get_session_not_found(self, db_session: AsyncSession) -> None:
248 repo = await _db_repo(db_session)
249 await db_session.commit()
250
251 result = await musehub_repository.get_session(db_session, repo.repo_id, "nonexistent")
252 assert result is None
253
254 @pytest.mark.anyio
255 async def test_get_session_wrong_repo_returns_none(self, db_session: AsyncSession) -> None:
256 repo1 = await _db_repo(db_session)
257 repo2 = await _db_repo(db_session)
258 sess = await _db_session(db_session, repo1.repo_id)
259 await db_session.commit()
260
261 result = await musehub_repository.get_session(db_session, repo2.repo_id, sess.session_id)
262 assert result is None
263
264 @pytest.mark.anyio
265 async def test_stop_session_marks_ended(self, db_session: AsyncSession) -> None:
266 repo = await _db_repo(db_session)
267 sess = await _db_session(db_session, repo.repo_id, is_active=True)
268 await db_session.commit()
269
270 result = await musehub_repository.stop_session(
271 db_session, repo.repo_id, sess.session_id, ended_at=None
272 )
273 assert result.is_active is False
274 assert result.ended_at is not None
275
276 @pytest.mark.anyio
277 async def test_stop_session_not_found_returns_none(self, db_session: AsyncSession) -> None:
278 repo = await _db_repo(db_session)
279 await db_session.commit()
280
281 result = await musehub_repository.stop_session(
282 db_session, repo.repo_id, "nonexistent-id", ended_at=None
283 )
284 assert result is None
285
286 @pytest.mark.anyio
287 async def test_stop_session_idempotent(self, db_session: AsyncSession) -> None:
288 repo = await _db_repo(db_session)
289 sess = await _db_session(db_session, repo.repo_id, is_active=True)
290 await db_session.commit()
291
292 t = _now()
293 await musehub_repository.stop_session(
294 db_session, repo.repo_id, sess.session_id, ended_at=t
295 )
296 # stop again — is_active already False, should not change ended_at
297 result2 = await musehub_repository.stop_session(
298 db_session, repo.repo_id, sess.session_id, ended_at=None
299 )
300 assert result2.is_active is False
301
302 @pytest.mark.anyio
303 async def test_stop_session_duration_computed(self, db_session: AsyncSession) -> None:
304 from datetime import timedelta
305
306 repo = await _db_repo(db_session)
307 started = datetime(2025, 1, 1, 10, 0, 0)
308 ended = datetime(2025, 1, 1, 11, 30, 0)
309 sess = MusehubSession(
310 session_id=_uid(),
311 repo_id=repo.repo_id,
312 started_at=started,
313 participants=[],
314 location="",
315 intent="",
316 is_active=True,
317 )
318 db_session.add(sess)
319 await db_session.commit()
320
321 result = await musehub_repository.stop_session(
322 db_session, repo.repo_id, sess.session_id, ended_at=ended
323 )
324 assert result.duration_seconds == 5400.0 # 90 minutes
325
326 @pytest.mark.anyio
327 async def test_musehub_sessions_service_upsert(self, db_session: AsyncSession) -> None:
328 repo = await _db_repo(db_session)
329 await db_session.commit()
330
331 sc = SessionCreate(participants=["dave"], intent="jam", location="garage")
332 resp = await musehub_sessions.upsert_session(db_session, repo.repo_id, sc)
333 assert resp.is_active is True
334 assert resp.participants == ["dave"]
335
336 @pytest.mark.anyio
337 async def test_musehub_sessions_service_list(self, db_session: AsyncSession) -> None:
338 repo = await _db_repo(db_session)
339 await _db_session(db_session, repo.repo_id)
340 await db_session.commit()
341
342 sessions, total = await musehub_sessions.list_sessions(db_session, repo.repo_id)
343 assert total == 1
344 assert len(sessions) == 1
345
346 @pytest.mark.anyio
347 async def test_musehub_sessions_service_get(self, db_session: AsyncSession) -> None:
348 repo = await _db_repo(db_session)
349 sess = await _db_session(db_session, repo.repo_id)
350 await db_session.commit()
351
352 result = await musehub_sessions.get_session(db_session, repo.repo_id, sess.session_id)
353 assert result is not None
354 assert result.session_id == sess.session_id
355
356 @pytest.mark.anyio
357 async def test_musehub_sessions_service_get_missing(self, db_session: AsyncSession) -> None:
358 repo = await _db_repo(db_session)
359 await db_session.commit()
360
361 result = await musehub_sessions.get_session(db_session, repo.repo_id, "bad-id")
362 assert result is None
363
364
365 # ═══════════════════════════════════════════════════════════════════════════════
366 # Layer 3 — End-to-End
367 # ═══════════════════════════════════════════════════════════════════════════════
368
369
370 class TestE2ESessions:
371 """Full HTTP stack via AsyncClient."""
372
373 @pytest.mark.anyio
374 async def test_create_session_201(
375 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
376 ) -> None:
377 repo = await _db_repo(db_session)
378 await db_session.commit()
379
380 resp = await client.post(
381 f"/api/repos/{repo.repo_id}/sessions",
382 json={"participants": ["alice"], "intent": "compose", "location": "home"},
383 headers=auth_headers,
384 )
385 assert resp.status_code == 201
386 data = resp.json()
387 assert data["isActive"] is True
388 assert "sessionId" in data
389
390 @pytest.mark.anyio
391 async def test_create_session_repo_not_found(
392 self, client: AsyncClient, auth_headers: StrDict
393 ) -> None:
394 resp = await client.post(
395 "/api/repos/nonexistent/sessions",
396 json={},
397 headers=auth_headers,
398 )
399 assert resp.status_code == 404
400
401 @pytest.mark.anyio
402 async def test_list_sessions_empty(
403 self, client: AsyncClient, db_session: AsyncSession
404 ) -> None:
405 repo = await _db_repo(db_session)
406 await db_session.commit()
407
408 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
409 assert resp.status_code == 200
410 data = resp.json()
411 assert data["total"] == 0
412 assert data["sessions"] == []
413
414 @pytest.mark.anyio
415 async def test_list_sessions_returns_created(
416 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
417 ) -> None:
418 repo = await _db_repo(db_session)
419 await db_session.commit()
420
421 await client.post(
422 f"/api/repos/{repo.repo_id}/sessions",
423 json={"participants": ["bob"], "intent": "record", "location": "studio"},
424 headers=auth_headers,
425 )
426
427 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
428 assert resp.status_code == 200
429 assert resp.json()["total"] == 1
430
431 @pytest.mark.anyio
432 async def test_list_sessions_limit_param(
433 self, client: AsyncClient, db_session: AsyncSession
434 ) -> None:
435 repo = await _db_repo(db_session)
436 for _ in range(5):
437 await _db_session(db_session, repo.repo_id)
438 await db_session.commit()
439
440 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions?limit=3")
441 assert resp.status_code == 200
442 data = resp.json()
443 assert data["total"] == 5
444 assert len(data["sessions"]) == 3
445
446 @pytest.mark.anyio
447 async def test_get_session_200(
448 self, client: AsyncClient, db_session: AsyncSession
449 ) -> None:
450 repo = await _db_repo(db_session)
451 sess = await _db_session(db_session, repo.repo_id)
452 await db_session.commit()
453
454 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions/{sess.session_id}")
455 assert resp.status_code == 200
456 data = resp.json()
457 assert data["sessionId"] == sess.session_id
458
459 @pytest.mark.anyio
460 async def test_get_session_not_found(
461 self, client: AsyncClient, db_session: AsyncSession
462 ) -> None:
463 repo = await _db_repo(db_session)
464 await db_session.commit()
465
466 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions/nonexistent")
467 assert resp.status_code == 404
468
469 @pytest.mark.anyio
470 async def test_stop_session_200(
471 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
472 ) -> None:
473 repo = await _db_repo(db_session)
474 await db_session.commit()
475
476 # create via HTTP
477 create_resp = await client.post(
478 f"/api/repos/{repo.repo_id}/sessions",
479 json={"intent": "test"},
480 headers=auth_headers,
481 )
482 assert create_resp.status_code == 201
483 session_id = create_resp.json()["sessionId"]
484
485 # stop it
486 stop_resp = await client.post(
487 f"/api/repos/{repo.repo_id}/sessions/{session_id}/stop",
488 json={},
489 headers=auth_headers,
490 )
491 assert stop_resp.status_code == 200
492 data = stop_resp.json()
493 assert data["isActive"] is False
494 assert data["endedAt"] is not None
495
496 @pytest.mark.anyio
497 async def test_stop_session_not_found(
498 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
499 ) -> None:
500 repo = await _db_repo(db_session)
501 await db_session.commit()
502
503 resp = await client.post(
504 f"/api/repos/{repo.repo_id}/sessions/nonexistent/stop",
505 json={},
506 headers=auth_headers,
507 )
508 assert resp.status_code == 404
509
510 @pytest.mark.anyio
511 async def test_stop_session_with_explicit_ended_at(
512 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
513 ) -> None:
514 repo = await _db_repo(db_session)
515 await db_session.commit()
516
517 create_resp = await client.post(
518 f"/api/repos/{repo.repo_id}/sessions",
519 json={},
520 headers=auth_headers,
521 )
522 session_id = create_resp.json()["sessionId"]
523
524 ended_at = "2025-06-01T12:00:00+00:00"
525 stop_resp = await client.post(
526 f"/api/repos/{repo.repo_id}/sessions/{session_id}/stop",
527 json={"endedAt": ended_at},
528 headers=auth_headers,
529 )
530 assert stop_resp.status_code == 200
531 assert stop_resp.json()["isActive"] is False
532
533 @pytest.mark.anyio
534 async def test_list_sessions_invalid_limit(
535 self, client: AsyncClient, db_session: AsyncSession
536 ) -> None:
537 repo = await _db_repo(db_session)
538 await db_session.commit()
539
540 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions?limit=999")
541 assert resp.status_code == 422
542
543 @pytest.mark.anyio
544 async def test_list_sessions_repo_not_found(self, client: AsyncClient) -> None:
545 resp = await client.get("/api/repos/nonexistent/sessions")
546 assert resp.status_code == 404
547
548
549 # ═══════════════════════════════════════════════════════════════════════════════
550 # Layer 4 — Stress
551 # ═══════════════════════════════════════════════════════════════════════════════
552
553
554 class TestStressSessions:
555 @pytest.mark.anyio
556 async def test_create_many_sessions(
557 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
558 ) -> None:
559 repo = await _db_repo(db_session)
560 await db_session.commit()
561
562 n = 15
563 for _ in range(n):
564 resp = await client.post(
565 f"/api/repos/{repo.repo_id}/sessions",
566 json={"intent": "batch"},
567 headers=auth_headers,
568 )
569 assert resp.status_code == 201
570
571 list_resp = await client.get(f"/api/repos/{repo.repo_id}/sessions?limit=200")
572 assert list_resp.json()["total"] == n
573
574 @pytest.mark.anyio
575 async def test_list_sessions_large_repo(
576 self, client: AsyncClient, db_session: AsyncSession
577 ) -> None:
578 repo = await _db_repo(db_session)
579 for _ in range(60):
580 await _db_session(db_session, repo.repo_id)
581 await db_session.commit()
582
583 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
584 data = resp.json()
585 assert data["total"] == 60
586 assert len(data["sessions"]) == 50 # default limit
587
588
589 # ═══════════════════════════════════════════════════════════════════════════════
590 # Layer 5 — Data Integrity
591 # ═══════════════════════════════════════════════════════════════════════════════
592
593
594 class TestDataIntegritySessions:
595 @pytest.mark.anyio
596 async def test_session_persists_after_commit(self, db_session: AsyncSession) -> None:
597 from sqlalchemy import select
598
599 repo = await _db_repo(db_session)
600 sess = await _db_session(db_session, repo.repo_id)
601 await db_session.commit()
602
603 result = await db_session.execute(
604 select(MusehubSession).where(MusehubSession.session_id == sess.session_id)
605 )
606 row = result.scalar_one_or_none()
607 assert row is not None
608 assert row.repo_id == repo.repo_id
609
610 @pytest.mark.anyio
611 async def test_stop_session_updates_is_active_flag(self, db_session: AsyncSession) -> None:
612 from sqlalchemy import select
613
614 repo = await _db_repo(db_session)
615 sess = await _db_session(db_session, repo.repo_id, is_active=True)
616 await db_session.commit()
617
618 await musehub_repository.stop_session(
619 db_session, repo.repo_id, sess.session_id, ended_at=None
620 )
621 await db_session.commit()
622
623 result = await db_session.execute(
624 select(MusehubSession).where(MusehubSession.session_id == sess.session_id)
625 )
626 row = result.scalar_one()
627 assert row.is_active is False
628 assert row.ended_at is not None
629
630 @pytest.mark.anyio
631 async def test_participants_stored_as_list(self, db_session: AsyncSession) -> None:
632 from sqlalchemy import select
633
634 repo = await _db_repo(db_session)
635 participants = ["alice", "bob", "carol"]
636 sess = await _db_session(db_session, repo.repo_id, participants=participants)
637 await db_session.commit()
638
639 result = await db_session.execute(
640 select(MusehubSession).where(MusehubSession.session_id == sess.session_id)
641 )
642 row = result.scalar_one()
643 assert row.participants == participants
644
645 @pytest.mark.anyio
646 async def test_duration_correct_after_stop(self, db_session: AsyncSession) -> None:
647 from datetime import timedelta
648
649 repo = await _db_repo(db_session)
650 started = datetime(2025, 3, 1, 9, 0, 0)
651 ended = datetime(2025, 3, 1, 10, 0, 0) # 3600 seconds later
652 sess = MusehubSession(
653 session_id=_uid(),
654 repo_id=repo.repo_id,
655 started_at=started,
656 participants=[],
657 location="",
658 intent="",
659 is_active=True,
660 )
661 db_session.add(sess)
662 await db_session.commit()
663
664 result = await musehub_repository.stop_session(
665 db_session, repo.repo_id, sess.session_id, ended_at=ended
666 )
667 assert result.duration_seconds == 3600.0
668
669 @pytest.mark.anyio
670 async def test_session_scoped_to_repo(self, db_session: AsyncSession) -> None:
671 repo1 = await _db_repo(db_session)
672 repo2 = await _db_repo(db_session)
673 await _db_session(db_session, repo1.repo_id)
674 await _db_session(db_session, repo1.repo_id)
675 await _db_session(db_session, repo2.repo_id)
676 await db_session.commit()
677
678 sessions1, total1 = await musehub_repository.list_sessions(db_session, repo1.repo_id)
679 sessions2, total2 = await musehub_repository.list_sessions(db_session, repo2.repo_id)
680 assert total1 == 2
681 assert total2 == 1
682
683 @pytest.mark.anyio
684 async def test_stop_already_stopped_preserves_ended_at(self, db_session: AsyncSession) -> None:
685 repo = await _db_repo(db_session)
686 sess = await _db_session(db_session, repo.repo_id, is_active=True)
687 await db_session.commit()
688
689 t1 = datetime(2025, 6, 1, 10, 0, 0)
690 await musehub_repository.stop_session(db_session, repo.repo_id, sess.session_id, ended_at=t1)
691 await db_session.commit()
692
693 # Stop again — is_active is already False, so ended_at must NOT change
694 t2 = datetime(2025, 6, 1, 11, 0, 0)
695 result = await musehub_repository.stop_session(db_session, repo.repo_id, sess.session_id, ended_at=t2)
696 # ended_at stays as t1 (not overwritten because is_active was already False)
697 assert result.ended_at.replace(tzinfo=None) == t1
698
699
700 # ═══════════════════════════════════════════════════════════════════════════════
701 # Layer 6 — Security
702 # ═══════════════════════════════════════════════════════════════════════════════
703
704
705 class TestSecuritySessions:
706 @pytest.mark.anyio
707 async def test_create_session_requires_auth(
708 self, client: AsyncClient, db_session: AsyncSession
709 ) -> None:
710 repo = await _db_repo(db_session)
711 await db_session.commit()
712
713 resp = await client.post(
714 f"/api/repos/{repo.repo_id}/sessions",
715 json={"intent": "unauth"},
716 )
717 assert resp.status_code == 401
718
719 @pytest.mark.anyio
720 async def test_stop_session_requires_auth(
721 self, client: AsyncClient, db_session: AsyncSession
722 ) -> None:
723 repo = await _db_repo(db_session)
724 sess = await _db_session(db_session, repo.repo_id)
725 await db_session.commit()
726
727 resp = await client.post(
728 f"/api/repos/{repo.repo_id}/sessions/{sess.session_id}/stop",
729 json={},
730 )
731 assert resp.status_code == 401
732
733 @pytest.mark.anyio
734 async def test_list_sessions_public_repo_no_auth(
735 self, client: AsyncClient, db_session: AsyncSession
736 ) -> None:
737 repo = await _db_repo(db_session, visibility="public")
738 await _db_session(db_session, repo.repo_id)
739 await db_session.commit()
740
741 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
742 assert resp.status_code == 200
743
744 @pytest.mark.anyio
745 async def test_list_sessions_private_repo_no_auth_401(
746 self, client: AsyncClient, db_session: AsyncSession
747 ) -> None:
748 repo = await _db_repo(db_session, visibility="private")
749 await db_session.commit()
750
751 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
752 assert resp.status_code == 401
753
754 @pytest.mark.anyio
755 async def test_get_session_private_repo_no_auth_401(
756 self, client: AsyncClient, db_session: AsyncSession
757 ) -> None:
758 repo = await _db_repo(db_session, visibility="private")
759 sess = await _db_session(db_session, repo.repo_id)
760 await db_session.commit()
761
762 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions/{sess.session_id}")
763 assert resp.status_code == 401
764
765 @pytest.mark.anyio
766 async def test_cannot_stop_other_repos_session(
767 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
768 ) -> None:
769 """Session from repo1 cannot be stopped via repo2's endpoint."""
770 repo1 = await _db_repo(db_session)
771 repo2 = await _db_repo(db_session)
772 sess = await _db_session(db_session, repo1.repo_id)
773 await db_session.commit()
774
775 resp = await client.post(
776 f"/api/repos/{repo2.repo_id}/sessions/{sess.session_id}/stop",
777 json={},
778 headers=auth_headers,
779 )
780 assert resp.status_code == 404
781
782
783 # ═══════════════════════════════════════════════════════════════════════════════
784 # Layer 7 — Performance
785 # ═══════════════════════════════════════════════════════════════════════════════
786
787
788 class TestPerformanceSessions:
789 @pytest.mark.anyio
790 async def test_create_session_latency(
791 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
792 ) -> None:
793 repo = await _db_repo(db_session)
794 await db_session.commit()
795
796 start = time.perf_counter()
797 resp = await client.post(
798 f"/api/repos/{repo.repo_id}/sessions",
799 json={"intent": "perf"},
800 headers=auth_headers,
801 )
802 elapsed = time.perf_counter() - start
803
804 assert resp.status_code == 201
805 assert elapsed < 0.5
806
807 @pytest.mark.anyio
808 async def test_list_sessions_latency(
809 self, client: AsyncClient, db_session: AsyncSession
810 ) -> None:
811 repo = await _db_repo(db_session)
812 for _ in range(20):
813 await _db_session(db_session, repo.repo_id)
814 await db_session.commit()
815
816 start = time.perf_counter()
817 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
818 elapsed = time.perf_counter() - start
819
820 assert resp.status_code == 200
821 assert elapsed < 0.5
822
823 @pytest.mark.anyio
824 async def test_stop_session_latency(
825 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
826 ) -> None:
827 repo = await _db_repo(db_session)
828 await db_session.commit()
829
830 create_resp = await client.post(
831 f"/api/repos/{repo.repo_id}/sessions",
832 json={},
833 headers=auth_headers,
834 )
835 session_id = create_resp.json()["sessionId"]
836
837 start = time.perf_counter()
838 stop_resp = await client.post(
839 f"/api/repos/{repo.repo_id}/sessions/{session_id}/stop",
840 json={},
841 headers=auth_headers,
842 )
843 elapsed = time.perf_counter() - start
844
845 assert stop_resp.status_code == 200
846 assert elapsed < 0.5
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago