Files
hermes-agent/tests/hermes_cli/test_web_server_sessiondb_eventloop.py
kshitijk4poor 3402644764 test(web): expect skipped_active in bulk-delete response
The endpoint now reports rows refused for a live turn; update the
event-loop test stub expectation to the new response shape.
2026-09-27 20:49:04 +05:30

152 lines
4.7 KiB
Python

"""Dashboard session routes must run SessionDB open + work off the event loop.
Regression for #60747: /search, /stats, /{id} (and bulk delete) ran FTS and
SQLite inline on the loop thread, so a large state.db froze every other
dashboard request (rename too). These drive the real route handlers and record which
thread every SessionDB call lands on.
"""
import asyncio
import threading
import pytest
import hermes_cli.web_models as _web_models
import hermes_cli.web_routers.sessions as _rt_sessions
import hermes_cli.web_server_sessions as _web_server_sessions
def test_bulk_delete_sessiondb_work_runs_off_event_loop(monkeypatch):
loop_thread = threading.get_ident()
db_threads: list[int] = []
db_modes: list[bool] = []
class _DB:
def delete_sessions(self, ids, **kwargs):
db_threads.append(threading.get_ident())
assert ids == ["one", "two"]
return 2
def close(self):
db_threads.append(threading.get_ident())
def _open_db(profile=None, *, read_only):
assert profile is None
db_modes.append(read_only)
return _DB()
monkeypatch.setattr(_web_server_sessions, "_open_session_db_for_profile", _open_db)
result = asyncio.run(
_rt_sessions.bulk_delete_sessions_endpoint(
_web_models.BulkDeleteSessions(ids=["one", "two"])
)
)
assert result == {"ok": True, "deleted": 2, "skipped_active": []}
assert db_modes == [False]
assert db_threads
assert all(thread_id != loop_thread for thread_id in db_threads)
class _ReadDB:
"""Read-only stand-in that records which thread every SessionDB call runs on."""
def __init__(self, threads: list[int]):
self._threads = threads
def _hit(self, value=None):
self._threads.append(threading.get_ident())
return value
def session_count(self, **_kw):
return self._hit(3)
def message_count(self):
return self._hit(7)
def session_count_by_source(self, **_kw):
return self._hit({"cli": 3})
def get_session(self, sid):
return self._hit({"id": sid, "title": "t"})
def get_session_rich_row(self, sid):
return self._hit(None)
def get_compression_tip(self, root_id):
return self._hit(None)
def search_sessions_by_id(self, q, **_kw):
return self._hit([{"id": "sess-1", "preview": "hello", "started_at": 1.0}])
def search_messages(self, **_kw):
return self._hit([])
def close(self):
self._hit()
@pytest.mark.parametrize(
"call",
[
pytest.param(lambda: _rt_sessions.search_sessions(q="hello"), id="search"),
pytest.param(lambda: _rt_sessions.get_session_stats(), id="stats"),
pytest.param(lambda: _rt_sessions.get_session_detail("sess-1"), id="detail"),
],
)
def test_session_read_handlers_run_sessiondb_work_off_event_loop(monkeypatch, call):
"""Regression for #60747: /search, /stats and /{id} ran FTS + SQLite inline on the
loop thread, so a large state.db froze every other dashboard request."""
loop_thread = threading.get_ident()
db_threads: list[int] = []
def _open_db(profile=None, *, read_only):
assert read_only is True
return _ReadDB(db_threads)
monkeypatch.setattr(_web_server_sessions, "_open_session_db_for_profile", _open_db)
monkeypatch.setattr(_rt_sessions, "_resolve_session_id", lambda db, sid: sid)
asyncio.run(call())
assert db_threads, "handler never touched the SessionDB"
assert all(thread_id != loop_thread for thread_id in db_threads)
def test_session_rename_runs_writer_open_and_update_off_event_loop(monkeypatch):
loop_thread = threading.get_ident()
db_threads: list[int] = []
class _DB:
def set_session_title(self, sid, title):
db_threads.append(threading.get_ident())
assert (sid, title) == ("sess-1", "renamed")
def get_session_title(self, sid):
db_threads.append(threading.get_ident())
assert sid == "sess-1"
return "renamed"
def close(self):
db_threads.append(threading.get_ident())
def _open_db(profile=None, *, read_only):
db_threads.append(threading.get_ident())
assert profile is None
assert read_only is False
return _DB()
monkeypatch.setattr(_web_server_sessions, "_open_session_db_for_profile", _open_db)
monkeypatch.setattr(_rt_sessions, "_resolve_session_id", lambda db, sid: sid)
result = asyncio.run(
_rt_sessions.rename_session_endpoint(
"sess-1", _web_models.SessionRename(title="renamed")
)
)
assert result == {"ok": True, "title": "renamed"}
assert db_threads
assert all(thread_id != loop_thread for thread_id in db_threads)