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