gabriel / musehub public

test_sessions.py file-level

at sha256:3 · View file ↗ · Intel ↗

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