| """Unit tests for the optional durable session store abstraction.""" |
|
|
| from datetime import datetime |
|
|
| import pytest |
|
|
| from agent.core.session_persistence import ( |
| MongoSessionStore, |
| NoopSessionStore, |
| USAGE_EVENT_TYPES, |
| _safe_message_doc, |
| ) |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_noop_store_keeps_local_cli_and_tests_db_free(): |
| store = NoopSessionStore() |
|
|
| await store.init() |
| await store.upsert_session(session_id="s1", user_id="u1", model="m") |
| await store.save_snapshot( |
| session_id="s1", |
| user_id="u1", |
| model="m", |
| messages=[{"role": "user", "content": "hello"}], |
| ) |
|
|
| assert await store.load_session("s1") is None |
| assert await store.list_sessions("u1") == [] |
| assert await store.append_event("s1", "processing", {}) is None |
|
|
|
|
| def test_unsafe_message_payload_is_replaced_with_marker(): |
| marker = _safe_message_doc({"role": "assistant", "content": object()}) |
|
|
| assert marker["role"] == "tool" |
| assert marker["ml_intern_persistence_error"] == "message_too_large_or_invalid" |
|
|
|
|
| |
|
|
|
|
| class _FakeProUsers: |
| """In-memory stand-in for the ``pro_users`` collection. |
| |
| Supports just enough of the Motor API to exercise ``mark_pro_seen``: |
| ``update_one`` with ``$setOnInsert`` + ``$set`` + ``upsert=True``, and |
| ``find_one_and_update`` with the guarded filter the conversion check uses. |
| """ |
|
|
| def __init__(self) -> None: |
| self.docs: dict[str, dict] = {} |
|
|
| async def update_one(self, filt, update, upsert=False): |
| _id = filt["_id"] |
| doc = self.docs.get(_id) |
| if doc is None and upsert: |
| doc = dict(update.get("$setOnInsert") or {}) |
| self.docs[_id] = doc |
| if doc is None: |
| return |
| for k, v in (update.get("$set") or {}).items(): |
| doc[k] = v |
|
|
| async def find_one_and_update(self, filt, update, return_document=None): |
| _id = filt["_id"] |
| doc = self.docs.get(_id) |
| if doc is None: |
| return None |
| |
| |
| for k, v in filt.items(): |
| if k == "_id": |
| continue |
| if isinstance(v, dict) and "$exists" in v: |
| if v["$exists"] and k not in doc: |
| return None |
| if not v["$exists"] and k in doc: |
| return None |
| elif doc.get(k) != v: |
| return None |
| for k, v in (update.get("$set") or {}).items(): |
| doc[k] = v |
| return dict(doc) |
|
|
|
|
| class _FakeDB: |
| def __init__(self) -> None: |
| self.pro_users = _FakeProUsers() |
|
|
|
|
| def _store_with_fake_db() -> MongoSessionStore: |
| s = MongoSessionStore.__new__(MongoSessionStore) |
| s.enabled = True |
| s.db = _FakeDB() |
| return s |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_mark_pro_seen_returns_none_when_unknown_user_starts_pro(): |
| """Joining as Pro shouldn't count as a conversion.""" |
| store = _store_with_fake_db() |
| assert await store.mark_pro_seen("u-new-pro", is_pro=True) is None |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_mark_pro_seen_emits_conversion_after_seeing_user_as_free(): |
| store = _store_with_fake_db() |
| assert await store.mark_pro_seen("u1", is_pro=False) is None |
| result = await store.mark_pro_seen("u1", is_pro=True) |
| assert result is not None |
| assert result["converted"] is True |
| assert isinstance(result["first_seen_at"], str) |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_mark_pro_seen_only_fires_conversion_once(): |
| """Re-checking a converted user must not re-emit the event.""" |
| store = _store_with_fake_db() |
| await store.mark_pro_seen("u1", is_pro=False) |
| first = await store.mark_pro_seen("u1", is_pro=True) |
| assert first is not None and first["converted"] is True |
| second = await store.mark_pro_seen("u1", is_pro=True) |
| assert second is None |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_noop_store_mark_pro_seen_returns_none(): |
| store = NoopSessionStore() |
| assert await store.mark_pro_seen("u1", is_pro=True) is None |
| assert await store.mark_pro_seen("u1", is_pro=False) is None |
|
|
|
|
| |
|
|
|
|
| class _FakeAsyncCursor: |
| def __init__(self, docs): |
| self.docs = docs |
| self.sort_calls = [] |
|
|
| def sort(self, *args, **kwargs): |
| self.sort_calls.append((args, kwargs)) |
| return self |
|
|
| def __aiter__(self): |
| self._iter = iter(self.docs) |
| return self |
|
|
| async def __anext__(self): |
| try: |
| return next(self._iter) |
| except StopIteration: |
| raise StopAsyncIteration from None |
|
|
|
|
| class _FakeFindCollection: |
| def __init__(self, docs): |
| self.docs = docs |
| self.find_calls = [] |
| self.cursors = [] |
|
|
| def find(self, query, projection=None): |
| self.find_calls.append((query, projection)) |
| cursor = _FakeAsyncCursor(self.docs) |
| self.cursors.append(cursor) |
| return cursor |
|
|
|
|
| class _FakeUsageDB: |
| def __init__(self, *, sessions, events) -> None: |
| self.sessions = _FakeFindCollection(sessions) |
| self.session_events = _FakeFindCollection(events) |
|
|
|
|
| def _store_with_fake_usage_db(*, sessions, events) -> MongoSessionStore: |
| s = MongoSessionStore.__new__(MongoSessionStore) |
| s.enabled = True |
| s.db = _FakeUsageDB(sessions=sessions, events=events) |
| return s |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_load_usage_events_scopes_mongo_queries_to_current_user_and_window(): |
| start = datetime(2026, 6, 1, 0, 0) |
| end = datetime(2026, 7, 1, 0, 0) |
| store = _store_with_fake_usage_db( |
| sessions=[{"_id": "s1"}, {"_id": "s2"}], |
| events=[ |
| {"session_id": "s1", "event_type": "llm_call", "data": {"cost_usd": 1.0}} |
| ], |
| ) |
|
|
| events = await store.load_usage_events("owner", start=start, end=end) |
|
|
| assert events == [ |
| {"session_id": "s1", "event_type": "llm_call", "data": {"cost_usd": 1.0}} |
| ] |
| assert store.db.sessions.find_calls == [ |
| ( |
| {"visibility": {"$ne": "deleted"}, "user_id": "owner"}, |
| {"_id": 1}, |
| ) |
| ] |
| assert store.db.session_events.find_calls == [ |
| ( |
| { |
| "session_id": {"$in": ["s1", "s2"]}, |
| "event_type": {"$in": list(USAGE_EVENT_TYPES)}, |
| "created_at": {"$gte": start, "$lt": end}, |
| }, |
| None, |
| ) |
| ] |
| assert store.db.session_events.cursors[0].sort_calls == [(("created_at", 1), {})] |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_load_usage_events_dev_mode_uses_requested_session_without_user_filter(): |
| store = _store_with_fake_usage_db( |
| sessions=[{"_id": "s3"}], |
| events=[{"session_id": "s3", "event_type": "hf_job_complete", "data": {}}], |
| ) |
|
|
| events = await store.load_usage_events("dev", session_id="s3") |
|
|
| assert events == [{"session_id": "s3", "event_type": "hf_job_complete", "data": {}}] |
| assert store.db.sessions.find_calls == [ |
| ({"visibility": {"$ne": "deleted"}, "_id": "s3"}, {"_id": 1}) |
| ] |
| assert store.db.session_events.find_calls == [ |
| ( |
| { |
| "session_id": {"$in": ["s3"]}, |
| "event_type": {"$in": list(USAGE_EVENT_TYPES)}, |
| }, |
| None, |
| ) |
| ] |
|
|