test_musehub_sessions_service.py
python
| 1 | """Unit tests for musehub/services/musehub_sessions.py. |
| 2 | |
| 3 | Tests the service layer directly (no HTTP) to validate session creation, |
| 4 | listing, retrieval, and ordering semantics. |
| 5 | """ |
| 6 | from __future__ import annotations |
| 7 | |
| 8 | from datetime import datetime, timedelta, timezone |
| 9 | |
| 10 | import pytest |
| 11 | from sqlalchemy.ext.asyncio import AsyncSession |
| 12 | |
| 13 | from musehub.models.musehub import SessionCreate |
| 14 | from musehub.services import musehub_sessions |
| 15 | from tests.factories import create_repo |
| 16 | |
| 17 | |
| 18 | def _session_create(**kwargs: object) -> SessionCreate: |
| 19 | defaults = dict( |
| 20 | participants=["alice"], |
| 21 | intent="Recording session", |
| 22 | location="Studio A", |
| 23 | ) |
| 24 | defaults.update(kwargs) |
| 25 | return SessionCreate(**defaults) |
| 26 | |
| 27 | |
| 28 | @pytest.mark.anyio |
| 29 | async def test_upsert_session_creates_record(db_session: AsyncSession) -> None: |
| 30 | """upsert_session returns a SessionResponse with the expected fields.""" |
| 31 | repo = await create_repo(db_session, visibility="public") |
| 32 | data = _session_create(participants=["bob", "alice"]) |
| 33 | |
| 34 | result = await musehub_sessions.upsert_session(db_session, str(repo.repo_id), data) |
| 35 | |
| 36 | assert result.session_id is not None |
| 37 | assert set(result.participants) == {"bob", "alice"} |
| 38 | assert result.intent == "Recording session" |
| 39 | assert result.location == "Studio A" |
| 40 | assert result.is_active is True |
| 41 | |
| 42 | |
| 43 | @pytest.mark.anyio |
| 44 | async def test_upsert_session_uses_provided_started_at(db_session: AsyncSession) -> None: |
| 45 | """started_at from the request is preserved in the session record.""" |
| 46 | repo = await create_repo(db_session) |
| 47 | t = datetime(2026, 1, 15, 10, 0, 0, tzinfo=timezone.utc) |
| 48 | data = _session_create(started_at=t) |
| 49 | |
| 50 | result = await musehub_sessions.upsert_session(db_session, str(repo.repo_id), data) |
| 51 | await db_session.commit() |
| 52 | |
| 53 | assert result.started_at is not None |
| 54 | |
| 55 | |
| 56 | @pytest.mark.anyio |
| 57 | async def test_list_sessions_empty_for_new_repo(db_session: AsyncSession) -> None: |
| 58 | """A new repo with no sessions returns an empty list and total=0.""" |
| 59 | repo = await create_repo(db_session) |
| 60 | sessions, total = await musehub_sessions.list_sessions(db_session, str(repo.repo_id)) |
| 61 | assert sessions == [] |
| 62 | assert total == 0 |
| 63 | |
| 64 | |
| 65 | @pytest.mark.anyio |
| 66 | async def test_list_sessions_returns_all_sessions(db_session: AsyncSession) -> None: |
| 67 | """list_sessions returns all sessions belonging to a repo.""" |
| 68 | repo = await create_repo(db_session) |
| 69 | rid = str(repo.repo_id) |
| 70 | |
| 71 | for i in range(3): |
| 72 | await musehub_sessions.upsert_session( |
| 73 | db_session, rid, _session_create(intent=f"Session {i}") |
| 74 | ) |
| 75 | await db_session.commit() |
| 76 | |
| 77 | sessions, total = await musehub_sessions.list_sessions(db_session, rid) |
| 78 | assert total == 3 |
| 79 | assert len(sessions) == 3 |
| 80 | |
| 81 | |
| 82 | @pytest.mark.anyio |
| 83 | async def test_list_sessions_ordered_newest_first(db_session: AsyncSession) -> None: |
| 84 | """list_sessions returns sessions sorted by started_at descending.""" |
| 85 | repo = await create_repo(db_session) |
| 86 | rid = str(repo.repo_id) |
| 87 | now = datetime.now(tz=timezone.utc) |
| 88 | |
| 89 | for i in range(3): |
| 90 | t = now - timedelta(hours=i) |
| 91 | await musehub_sessions.upsert_session( |
| 92 | db_session, rid, _session_create(started_at=t, intent=f"Session {i}") |
| 93 | ) |
| 94 | await db_session.commit() |
| 95 | |
| 96 | sessions, _ = await musehub_sessions.list_sessions(db_session, rid) |
| 97 | timestamps = [s.started_at for s in sessions] |
| 98 | assert timestamps == sorted(timestamps, reverse=True) |
| 99 | |
| 100 | |
| 101 | @pytest.mark.anyio |
| 102 | async def test_list_sessions_isolates_by_repo(db_session: AsyncSession) -> None: |
| 103 | """Sessions from one repo do not appear when listing another repo's sessions.""" |
| 104 | repo_a = await create_repo(db_session, slug="repo-a-sess") |
| 105 | repo_b = await create_repo(db_session, slug="repo-b-sess") |
| 106 | |
| 107 | await musehub_sessions.upsert_session( |
| 108 | db_session, str(repo_a.repo_id), _session_create() |
| 109 | ) |
| 110 | await db_session.commit() |
| 111 | |
| 112 | sessions, total = await musehub_sessions.list_sessions(db_session, str(repo_b.repo_id)) |
| 113 | assert total == 0 |
| 114 | assert sessions == [] |
| 115 | |
| 116 | |
| 117 | @pytest.mark.anyio |
| 118 | async def test_get_session_by_id(db_session: AsyncSession) -> None: |
| 119 | """get_session returns the correct session given its ID.""" |
| 120 | repo = await create_repo(db_session) |
| 121 | rid = str(repo.repo_id) |
| 122 | |
| 123 | created = await musehub_sessions.upsert_session( |
| 124 | db_session, rid, _session_create(intent="Find me") |
| 125 | ) |
| 126 | await db_session.commit() |
| 127 | |
| 128 | found = await musehub_sessions.get_session(db_session, rid, created.session_id) |
| 129 | assert found is not None |
| 130 | assert found.session_id == created.session_id |
| 131 | assert found.intent == "Find me" |
| 132 | |
| 133 | |
| 134 | @pytest.mark.anyio |
| 135 | async def test_get_session_nonexistent_returns_none(db_session: AsyncSession) -> None: |
| 136 | """get_session returns None for an unknown session ID.""" |
| 137 | repo = await create_repo(db_session) |
| 138 | result = await musehub_sessions.get_session( |
| 139 | db_session, str(repo.repo_id), "00000000-0000-0000-0000-000000000000" |
| 140 | ) |
| 141 | assert result is None |
| 142 | |
| 143 | |
| 144 | @pytest.mark.anyio |
| 145 | async def test_list_sessions_limit_respected(db_session: AsyncSession) -> None: |
| 146 | """list_sessions respects the limit parameter.""" |
| 147 | repo = await create_repo(db_session) |
| 148 | rid = str(repo.repo_id) |
| 149 | |
| 150 | for i in range(5): |
| 151 | await musehub_sessions.upsert_session(db_session, rid, _session_create()) |
| 152 | await db_session.commit() |
| 153 | |
| 154 | sessions, total = await musehub_sessions.list_sessions(db_session, rid, limit=2) |
| 155 | assert total == 5 |
| 156 | assert len(sessions) == 2 |