"""Tests for GET /api/seances/recent — the Ghost Log recap (usability wave, Workstream C, #7).""" from datetime import datetime, timedelta, timezone import pytest from app.models.contact_session import ContactSession from app.models.entity import Entity from app.models.event import Event async def _register(client, username): resp = await client.post( "/auth/register", json={"username": username, "password": "spookyspooky"} ) assert resp.status_code == 201 return resp.json()["id"] async def _login(client, username): resp = await client.post( "/auth/login", json={"username": username, "password": "spookyspooky"} ) assert resp.status_code == 200 def _entity(name, signature): return Entity( name=name, epithet="the Test Wraith", rarity_tier="rare", signature=signature, visual_profile={"hue": 200, "form": "wisp"}, ) @pytest.mark.asyncio async def test_unauthenticated_rejected(client): resp = await client.get("/api/seances/recent") assert resp.status_code == 401 @pytest.mark.asyncio async def test_only_own_sessions_newest_first_with_counts_and_echoes(client, db_session): my_id = await _register(client, "seeker1") other_id = await _register(client, "seeker2") await _login(client, "seeker1") entity = _entity("Testnamed", "sig-aaaa") db_session.add(entity) await db_session.flush() now = datetime.now(timezone.utc) older = ContactSession( user_id=my_id, entity_id=entity.id, mode="evp", started_at=now - timedelta(hours=2) ) newer = ContactSession( user_id=my_id, entity_id=entity.id, mode="wire", started_at=now - timedelta(hours=1) ) theirs = ContactSession(user_id=other_id, mode="ouija", started_at=now) db_session.add_all([older, newer, theirs]) await db_session.flush() # Events on the newer session: 2 questions, 3 anomalies, 4 spirit # utterances (only the newest 3 should come back as echoes). events = [ Event(session_id=newer.id, kind="question", text="who's there?"), Event(session_id=newer.id, kind="question", text="still there?"), Event(session_id=newer.id, kind="anomaly", payload={"score": 1}), Event(session_id=newer.id, kind="anomaly", payload={"score": 2}), Event(session_id=newer.id, kind="anomaly", payload={"score": 3}), ] for i in range(4): events.append( Event( session_id=newer.id, kind="utterance", text=f"echo {i}", payload={"kind": "reply"}, created_at=now - timedelta(minutes=30 - i), ) ) # An utterance on the OTHER user's session must never leak. events.append( Event( session_id=theirs.id, kind="utterance", text="not yours", payload={"kind": "greeting"}, ) ) db_session.add_all(events) await db_session.commit() resp = await client.get("/api/seances/recent") assert resp.status_code == 200 seances = resp.json()["seances"] # Only my sessions, newest first. assert [s["id"] for s in seances] == [str(newer.id), str(older.id)] first = seances[0] assert first["mode"] == "wire" assert first["counts"] == {"question": 2, "anomaly": 3, "utterance": 4} assert first["echoes"] == ["echo 3", "echo 2", "echo 1"] assert first["entity"] == { "id": str(entity.id), "name": "Testnamed", "epithet": "the Test Wraith", "rarity": "rare", "visual": {"hue": 200, "form": "wisp"}, } # No cross-user text anywhere in the response. assert "not yours" not in resp.text @pytest.mark.asyncio async def test_session_without_entity_does_not_crash(client, db_session): my_id = await _register(client, "seeker3") await _login(client, "seeker3") session = ContactSession(user_id=my_id, mode="ouija") db_session.add(session) await db_session.commit() resp = await client.get("/api/seances/recent") assert resp.status_code == 200 seances = resp.json()["seances"] assert len(seances) == 1 assert seances[0]["entity"] is None assert seances[0]["counts"] == {} assert seances[0]["echoes"] == [] @pytest.mark.asyncio async def test_limited_to_twelve(client, db_session): my_id = await _register(client, "seeker4") await _login(client, "seeker4") now = datetime.now(timezone.utc) db_session.add_all( ContactSession(user_id=my_id, mode="wire", started_at=now - timedelta(minutes=i)) for i in range(15) ) await db_session.commit() resp = await client.get("/api/seances/recent") assert resp.status_code == 200 assert len(resp.json()["seances"]) == 12