The endpoint now reports rows refused for a live turn; update the event-loop test stub expectation to the new response shape.
152 lines
4.7 KiB
Python
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)
|