gabriel / musehub public
test_sessions.py python
801 lines 30.1 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 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.types.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 async def test_create_session_returns_response(self, db_session: AsyncSession) -> None:
176 repo = await _db_repo(db_session)
177 await db_session.commit()
178
179 resp = await musehub_repository.create_session(
180 db_session,
181 repo.repo_id,
182 started_at=None,
183 participants=["alice"],
184 intent="write",
185 location="studio",
186 )
187 assert isinstance(resp, SessionResponse)
188 assert resp.is_active is True
189 assert resp.session_id is not None
190
191 async def test_create_session_uses_provided_started_at(self, db_session: AsyncSession) -> None:
192 repo = await _db_repo(db_session)
193 await db_session.commit()
194 t = _now()
195
196 resp = await musehub_repository.create_session(
197 db_session, repo.repo_id, started_at=t,
198 participants=[], intent="", location="",
199 )
200 # started_at stored as UTC; compare without tz
201 assert resp.started_at.replace(tzinfo=None) == t.replace(tzinfo=None)
202
203 async def test_list_sessions_empty(self, db_session: AsyncSession) -> None:
204 repo = await _db_repo(db_session)
205 await db_session.commit()
206
207 sessions, total, _ = await musehub_repository.list_sessions(db_session, repo.repo_id)
208 assert sessions == []
209 assert total == 0
210
211 async def test_list_sessions_returns_all(self, db_session: AsyncSession) -> None:
212 repo = await _db_repo(db_session)
213 await _db_session(db_session, repo.repo_id)
214 await _db_session(db_session, repo.repo_id)
215 await db_session.commit()
216
217 sessions, total, _ = await musehub_repository.list_sessions(db_session, repo.repo_id)
218 assert total == 2
219 assert len(sessions) == 2
220
221 async def test_list_sessions_limit(self, db_session: AsyncSession) -> None:
222 repo = await _db_repo(db_session)
223 for _ in range(5):
224 await _db_session(db_session, repo.repo_id)
225 await db_session.commit()
226
227 sessions, total, _ = await musehub_repository.list_sessions(db_session, repo.repo_id, limit=3)
228 assert total == 5
229 assert len(sessions) == 3
230
231 async def test_get_session_found(self, db_session: AsyncSession) -> None:
232 repo = await _db_repo(db_session)
233 sess = await _db_session(db_session, repo.repo_id)
234 await db_session.commit()
235
236 result = await musehub_repository.get_session(db_session, repo.repo_id, sess.session_id)
237 assert result is not None
238 assert result.session_id == sess.session_id
239
240 async def test_get_session_not_found(self, db_session: AsyncSession) -> None:
241 repo = await _db_repo(db_session)
242 await db_session.commit()
243
244 result = await musehub_repository.get_session(db_session, repo.repo_id, "nonexistent")
245 assert result is None
246
247 async def test_get_session_wrong_repo_returns_none(self, db_session: AsyncSession) -> None:
248 repo1 = await _db_repo(db_session)
249 repo2 = await _db_repo(db_session)
250 sess = await _db_session(db_session, repo1.repo_id)
251 await db_session.commit()
252
253 result = await musehub_repository.get_session(db_session, repo2.repo_id, sess.session_id)
254 assert result is None
255
256 async def test_stop_session_marks_ended(self, db_session: AsyncSession) -> None:
257 repo = await _db_repo(db_session)
258 sess = await _db_session(db_session, repo.repo_id, is_active=True)
259 await db_session.commit()
260
261 result = await musehub_repository.stop_session(
262 db_session, repo.repo_id, sess.session_id, ended_at=None
263 )
264 assert result.is_active is False
265 assert result.ended_at is not None
266
267 async def test_stop_session_not_found_returns_none(self, db_session: AsyncSession) -> None:
268 repo = await _db_repo(db_session)
269 await db_session.commit()
270
271 result = await musehub_repository.stop_session(
272 db_session, repo.repo_id, "nonexistent-id", ended_at=None
273 )
274 assert result is None
275
276 async def test_stop_session_idempotent(self, db_session: AsyncSession) -> None:
277 repo = await _db_repo(db_session)
278 sess = await _db_session(db_session, repo.repo_id, is_active=True)
279 await db_session.commit()
280
281 t = _now()
282 await musehub_repository.stop_session(
283 db_session, repo.repo_id, sess.session_id, ended_at=t
284 )
285 # stop again — is_active already False, should not change ended_at
286 result2 = await musehub_repository.stop_session(
287 db_session, repo.repo_id, sess.session_id, ended_at=None
288 )
289 assert result2.is_active is False
290
291 async def test_stop_session_duration_computed(self, db_session: AsyncSession) -> None:
292 from datetime import timedelta
293
294 repo = await _db_repo(db_session)
295 started = datetime(2025, 1, 1, 10, 0, 0)
296 ended = datetime(2025, 1, 1, 11, 30, 0)
297 sess = MusehubSession(
298 session_id=_uid(),
299 repo_id=repo.repo_id,
300 started_at=started,
301 participants=[],
302 location="",
303 intent="",
304 is_active=True,
305 )
306 db_session.add(sess)
307 await db_session.commit()
308
309 result = await musehub_repository.stop_session(
310 db_session, repo.repo_id, sess.session_id, ended_at=ended
311 )
312 assert result.duration_seconds == 5400.0 # 90 minutes
313
314 async def test_musehub_sessions_service_upsert(self, db_session: AsyncSession) -> None:
315 repo = await _db_repo(db_session)
316 await db_session.commit()
317
318 sc = SessionCreate(participants=["dave"], intent="jam", location="garage")
319 resp = await musehub_sessions.upsert_session(db_session, repo.repo_id, sc)
320 assert resp.is_active is True
321 assert resp.participants == ["dave"]
322
323 async def test_musehub_sessions_service_list(self, db_session: AsyncSession) -> None:
324 repo = await _db_repo(db_session)
325 await _db_session(db_session, repo.repo_id)
326 await db_session.commit()
327
328 sessions, total, _ = await musehub_sessions.list_sessions(db_session, repo.repo_id)
329 assert total == 1
330 assert len(sessions) == 1
331
332 async def test_musehub_sessions_service_get(self, db_session: AsyncSession) -> None:
333 repo = await _db_repo(db_session)
334 sess = await _db_session(db_session, repo.repo_id)
335 await db_session.commit()
336
337 result = await musehub_sessions.get_session(db_session, repo.repo_id, sess.session_id)
338 assert result is not None
339 assert result.session_id == sess.session_id
340
341 async def test_musehub_sessions_service_get_missing(self, db_session: AsyncSession) -> None:
342 repo = await _db_repo(db_session)
343 await db_session.commit()
344
345 result = await musehub_sessions.get_session(db_session, repo.repo_id, "bad-id")
346 assert result is None
347
348
349 # ═══════════════════════════════════════════════════════════════════════════════
350 # Layer 3 — End-to-End
351 # ═══════════════════════════════════════════════════════════════════════════════
352
353
354 class TestE2ESessions:
355 """Full HTTP stack via AsyncClient."""
356
357 async def test_create_session_201(
358 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
359 ) -> None:
360 repo = await _db_repo(db_session)
361 await db_session.commit()
362
363 resp = await client.post(
364 f"/api/repos/{repo.repo_id}/sessions",
365 json={"participants": ["alice"], "intent": "compose", "location": "home"},
366 headers=auth_headers,
367 )
368 assert resp.status_code == 201
369 data = resp.json()
370 assert data["isActive"] is True
371 assert "sessionId" in data
372
373 async def test_create_session_repo_not_found(
374 self, client: AsyncClient, auth_headers: StrDict
375 ) -> None:
376 resp = await client.post(
377 "/api/repos/nonexistent/sessions",
378 json={},
379 headers=auth_headers,
380 )
381 assert resp.status_code == 404
382
383 async def test_list_sessions_empty(
384 self, client: AsyncClient, db_session: AsyncSession
385 ) -> None:
386 repo = await _db_repo(db_session)
387 await db_session.commit()
388
389 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
390 assert resp.status_code == 200
391 data = resp.json()
392 assert data["total"] == 0
393 assert data["sessions"] == []
394
395 async def test_list_sessions_returns_created(
396 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
397 ) -> None:
398 repo = await _db_repo(db_session)
399 await db_session.commit()
400
401 await client.post(
402 f"/api/repos/{repo.repo_id}/sessions",
403 json={"participants": ["bob"], "intent": "record", "location": "studio"},
404 headers=auth_headers,
405 )
406
407 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
408 assert resp.status_code == 200
409 assert resp.json()["total"] == 1
410
411 async def test_list_sessions_limit_param(
412 self, client: AsyncClient, db_session: AsyncSession
413 ) -> None:
414 repo = await _db_repo(db_session)
415 for _ in range(5):
416 await _db_session(db_session, repo.repo_id)
417 await db_session.commit()
418
419 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions?limit=3")
420 assert resp.status_code == 200
421 data = resp.json()
422 assert data["total"] == 5
423 assert len(data["sessions"]) == 3
424
425 async def test_get_session_200(
426 self, client: AsyncClient, db_session: AsyncSession
427 ) -> None:
428 repo = await _db_repo(db_session)
429 sess = await _db_session(db_session, repo.repo_id)
430 await db_session.commit()
431
432 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions/{sess.session_id}")
433 assert resp.status_code == 200
434 data = resp.json()
435 assert data["sessionId"] == sess.session_id
436
437 async def test_get_session_not_found(
438 self, client: AsyncClient, db_session: AsyncSession
439 ) -> None:
440 repo = await _db_repo(db_session)
441 await db_session.commit()
442
443 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions/nonexistent")
444 assert resp.status_code == 404
445
446 async def test_stop_session_200(
447 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
448 ) -> None:
449 repo = await _db_repo(db_session)
450 await db_session.commit()
451
452 # create via HTTP
453 create_resp = await client.post(
454 f"/api/repos/{repo.repo_id}/sessions",
455 json={"intent": "test"},
456 headers=auth_headers,
457 )
458 assert create_resp.status_code == 201
459 session_id = create_resp.json()["sessionId"]
460
461 # stop it
462 stop_resp = await client.post(
463 f"/api/repos/{repo.repo_id}/sessions/{session_id}/stop",
464 json={},
465 headers=auth_headers,
466 )
467 assert stop_resp.status_code == 200
468 data = stop_resp.json()
469 assert data["isActive"] is False
470 assert data["endedAt"] is not None
471
472 async def test_stop_session_not_found(
473 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
474 ) -> None:
475 repo = await _db_repo(db_session)
476 await db_session.commit()
477
478 resp = await client.post(
479 f"/api/repos/{repo.repo_id}/sessions/nonexistent/stop",
480 json={},
481 headers=auth_headers,
482 )
483 assert resp.status_code == 404
484
485 async def test_stop_session_with_explicit_ended_at(
486 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
487 ) -> None:
488 repo = await _db_repo(db_session)
489 await db_session.commit()
490
491 create_resp = await client.post(
492 f"/api/repos/{repo.repo_id}/sessions",
493 json={},
494 headers=auth_headers,
495 )
496 session_id = create_resp.json()["sessionId"]
497
498 ended_at = "2025-06-01T12:00:00+00:00"
499 stop_resp = await client.post(
500 f"/api/repos/{repo.repo_id}/sessions/{session_id}/stop",
501 json={"endedAt": ended_at},
502 headers=auth_headers,
503 )
504 assert stop_resp.status_code == 200
505 assert stop_resp.json()["isActive"] is False
506
507 async def test_list_sessions_invalid_limit(
508 self, client: AsyncClient, db_session: AsyncSession
509 ) -> None:
510 repo = await _db_repo(db_session)
511 await db_session.commit()
512
513 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions?limit=999")
514 assert resp.status_code == 422
515
516 async def test_list_sessions_repo_not_found(self, client: AsyncClient) -> None:
517 resp = await client.get("/api/repos/nonexistent/sessions")
518 assert resp.status_code == 404
519
520
521 # ═══════════════════════════════════════════════════════════════════════════════
522 # Layer 4 — Stress
523 # ═══════════════════════════════════════════════════════════════════════════════
524
525
526 class TestStressSessions:
527 async def test_create_many_sessions(
528 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
529 ) -> None:
530 repo = await _db_repo(db_session)
531 await db_session.commit()
532
533 n = 15
534 for _ in range(n):
535 resp = await client.post(
536 f"/api/repos/{repo.repo_id}/sessions",
537 json={"intent": "batch"},
538 headers=auth_headers,
539 )
540 assert resp.status_code == 201
541
542 list_resp = await client.get(f"/api/repos/{repo.repo_id}/sessions?limit=200")
543 assert list_resp.json()["total"] == n
544
545 async def test_list_sessions_large_repo(
546 self, client: AsyncClient, db_session: AsyncSession
547 ) -> None:
548 repo = await _db_repo(db_session)
549 for _ in range(60):
550 await _db_session(db_session, repo.repo_id)
551 await db_session.commit()
552
553 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
554 data = resp.json()
555 assert data["total"] == 60
556 assert len(data["sessions"]) == 50 # default limit
557
558
559 # ═══════════════════════════════════════════════════════════════════════════════
560 # Layer 5 — Data Integrity
561 # ═══════════════════════════════════════════════════════════════════════════════
562
563
564 class TestDataIntegritySessions:
565 async def test_session_persists_after_commit(self, db_session: AsyncSession) -> None:
566 from sqlalchemy import select
567
568 repo = await _db_repo(db_session)
569 sess = await _db_session(db_session, repo.repo_id)
570 await db_session.commit()
571
572 result = await db_session.execute(
573 select(MusehubSession).where(MusehubSession.session_id == sess.session_id)
574 )
575 row = result.scalar_one_or_none()
576 assert row is not None
577 assert row.repo_id == repo.repo_id
578
579 async def test_stop_session_updates_is_active_flag(self, db_session: AsyncSession) -> None:
580 from sqlalchemy import select
581
582 repo = await _db_repo(db_session)
583 sess = await _db_session(db_session, repo.repo_id, is_active=True)
584 await db_session.commit()
585
586 await musehub_repository.stop_session(
587 db_session, repo.repo_id, sess.session_id, ended_at=None
588 )
589 await db_session.commit()
590
591 result = await db_session.execute(
592 select(MusehubSession).where(MusehubSession.session_id == sess.session_id)
593 )
594 row = result.scalar_one()
595 assert row.is_active is False
596 assert row.ended_at is not None
597
598 async def test_participants_stored_as_list(self, db_session: AsyncSession) -> None:
599 from sqlalchemy import select
600
601 repo = await _db_repo(db_session)
602 participants = ["alice", "bob", "carol"]
603 sess = await _db_session(db_session, repo.repo_id, participants=participants)
604 await db_session.commit()
605
606 result = await db_session.execute(
607 select(MusehubSession).where(MusehubSession.session_id == sess.session_id)
608 )
609 row = result.scalar_one()
610 assert row.participants == participants
611
612 async def test_duration_correct_after_stop(self, db_session: AsyncSession) -> None:
613 from datetime import timedelta
614
615 repo = await _db_repo(db_session)
616 started = datetime(2025, 3, 1, 9, 0, 0)
617 ended = datetime(2025, 3, 1, 10, 0, 0) # 3600 seconds later
618 sess = MusehubSession(
619 session_id=_uid(),
620 repo_id=repo.repo_id,
621 started_at=started,
622 participants=[],
623 location="",
624 intent="",
625 is_active=True,
626 )
627 db_session.add(sess)
628 await db_session.commit()
629
630 result = await musehub_repository.stop_session(
631 db_session, repo.repo_id, sess.session_id, ended_at=ended
632 )
633 assert result.duration_seconds == 3600.0
634
635 async def test_session_scoped_to_repo(self, db_session: AsyncSession) -> None:
636 repo1 = await _db_repo(db_session)
637 repo2 = await _db_repo(db_session)
638 await _db_session(db_session, repo1.repo_id)
639 await _db_session(db_session, repo1.repo_id)
640 await _db_session(db_session, repo2.repo_id)
641 await db_session.commit()
642
643 sessions1, total1, _ = await musehub_repository.list_sessions(db_session, repo1.repo_id)
644 sessions2, total2, _ = await musehub_repository.list_sessions(db_session, repo2.repo_id)
645 assert total1 == 2
646 assert total2 == 1
647
648 async def test_stop_already_stopped_preserves_ended_at(self, db_session: AsyncSession) -> None:
649 repo = await _db_repo(db_session)
650 sess = await _db_session(db_session, repo.repo_id, is_active=True)
651 await db_session.commit()
652
653 t1 = datetime(2025, 6, 1, 10, 0, 0)
654 await musehub_repository.stop_session(db_session, repo.repo_id, sess.session_id, ended_at=t1)
655 await db_session.commit()
656
657 # Stop again — is_active is already False, so ended_at must NOT change
658 t2 = datetime(2025, 6, 1, 11, 0, 0)
659 result = await musehub_repository.stop_session(db_session, repo.repo_id, sess.session_id, ended_at=t2)
660 # ended_at stays as t1 (not overwritten because is_active was already False)
661 assert result.ended_at.replace(tzinfo=None) == t1
662
663
664 # ═══════════════════════════════════════════════════════════════════════════════
665 # Layer 6 — Security
666 # ═══════════════════════════════════════════════════════════════════════════════
667
668
669 class TestSecuritySessions:
670 async def test_create_session_requires_auth(
671 self, client: AsyncClient, db_session: AsyncSession
672 ) -> None:
673 repo = await _db_repo(db_session)
674 await db_session.commit()
675
676 resp = await client.post(
677 f"/api/repos/{repo.repo_id}/sessions",
678 json={"intent": "unauth"},
679 )
680 assert resp.status_code == 401
681
682 async def test_stop_session_requires_auth(
683 self, client: AsyncClient, db_session: AsyncSession
684 ) -> None:
685 repo = await _db_repo(db_session)
686 sess = await _db_session(db_session, repo.repo_id)
687 await db_session.commit()
688
689 resp = await client.post(
690 f"/api/repos/{repo.repo_id}/sessions/{sess.session_id}/stop",
691 json={},
692 )
693 assert resp.status_code == 401
694
695 async def test_list_sessions_public_repo_no_auth(
696 self, client: AsyncClient, db_session: AsyncSession
697 ) -> None:
698 repo = await _db_repo(db_session, visibility="public")
699 await _db_session(db_session, repo.repo_id)
700 await db_session.commit()
701
702 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
703 assert resp.status_code == 200
704
705 async def test_list_sessions_private_repo_no_auth_401(
706 self, client: AsyncClient, db_session: AsyncSession
707 ) -> None:
708 repo = await _db_repo(db_session, visibility="private")
709 await db_session.commit()
710
711 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
712 assert resp.status_code == 401
713
714 async def test_get_session_private_repo_no_auth_401(
715 self, client: AsyncClient, db_session: AsyncSession
716 ) -> None:
717 repo = await _db_repo(db_session, visibility="private")
718 sess = await _db_session(db_session, repo.repo_id)
719 await db_session.commit()
720
721 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions/{sess.session_id}")
722 assert resp.status_code == 401
723
724 async def test_cannot_stop_other_repos_session(
725 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
726 ) -> None:
727 """Session from repo1 cannot be stopped via repo2's endpoint."""
728 repo1 = await _db_repo(db_session)
729 repo2 = await _db_repo(db_session)
730 sess = await _db_session(db_session, repo1.repo_id)
731 await db_session.commit()
732
733 resp = await client.post(
734 f"/api/repos/{repo2.repo_id}/sessions/{sess.session_id}/stop",
735 json={},
736 headers=auth_headers,
737 )
738 assert resp.status_code == 404
739
740
741 # ═══════════════════════════════════════════════════════════════════════════════
742 # Layer 7 — Performance
743 # ═══════════════════════════════════════════════════════════════════════════════
744
745
746 class TestPerformanceSessions:
747 async def test_create_session_latency(
748 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
749 ) -> None:
750 repo = await _db_repo(db_session)
751 await db_session.commit()
752
753 start = time.perf_counter()
754 resp = await client.post(
755 f"/api/repos/{repo.repo_id}/sessions",
756 json={"intent": "perf"},
757 headers=auth_headers,
758 )
759 elapsed = time.perf_counter() - start
760
761 assert resp.status_code == 201
762 assert elapsed < 0.5
763
764 async def test_list_sessions_latency(
765 self, client: AsyncClient, db_session: AsyncSession
766 ) -> None:
767 repo = await _db_repo(db_session)
768 for _ in range(20):
769 await _db_session(db_session, repo.repo_id)
770 await db_session.commit()
771
772 start = time.perf_counter()
773 resp = await client.get(f"/api/repos/{repo.repo_id}/sessions")
774 elapsed = time.perf_counter() - start
775
776 assert resp.status_code == 200
777 assert elapsed < 0.5
778
779 async def test_stop_session_latency(
780 self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict
781 ) -> None:
782 repo = await _db_repo(db_session)
783 await db_session.commit()
784
785 create_resp = await client.post(
786 f"/api/repos/{repo.repo_id}/sessions",
787 json={},
788 headers=auth_headers,
789 )
790 session_id = create_resp.json()["sessionId"]
791
792 start = time.perf_counter()
793 stop_resp = await client.post(
794 f"/api/repos/{repo.repo_id}/sessions/{session_id}/stop",
795 json={},
796 headers=auth_headers,
797 )
798 elapsed = time.perf_counter() - start
799
800 assert stop_resp.status_code == 200
801 assert elapsed < 0.5
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago