184 lines
6.4 KiB
Python
184 lines
6.4 KiB
Python
"""Regression coverage for #2965 streaming SessionDB profile isolation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import queue
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
if str(REPO) not in sys.path:
|
|
sys.path.insert(0, str(REPO))
|
|
|
|
|
|
def test_streaming_sessiondb_uses_session_profile_state_db(tmp_path, monkeypatch):
|
|
"""WebUI streaming must not rely on hermes_state.DEFAULT_DB_PATH.
|
|
|
|
``hermes_state.DEFAULT_DB_PATH`` is frozen at import time. A non-default
|
|
WebUI profile therefore needs the streaming attach path to pass the already
|
|
resolved profile home to SessionDB explicitly.
|
|
"""
|
|
from api import config as cfg
|
|
from api import oauth
|
|
from api import profiles
|
|
from api import streaming
|
|
|
|
profile_home = tmp_path / "hermes-home" / "profiles" / "zhangtingban"
|
|
profile_home.mkdir(parents=True)
|
|
|
|
class FakeSession:
|
|
session_id = "issue2965-session"
|
|
title = "Issue 2965"
|
|
workspace = str(tmp_path)
|
|
model = "test-model"
|
|
model_provider = None
|
|
profile = "zhangtingban"
|
|
personality = None
|
|
messages = []
|
|
context_messages = []
|
|
tool_calls = []
|
|
input_tokens = 0
|
|
output_tokens = 0
|
|
estimated_cost = None
|
|
context_length = 0
|
|
threshold_tokens = 0
|
|
last_prompt_tokens = 0
|
|
active_stream_id = "issue2965-stream"
|
|
pending_user_message = None
|
|
pending_attachments = []
|
|
pending_started_at = None
|
|
llm_title_generated = True
|
|
|
|
def save(self, *args, **kwargs):
|
|
return None
|
|
|
|
def compact(self):
|
|
return {
|
|
"session_id": self.session_id,
|
|
"title": self.title,
|
|
"workspace": self.workspace,
|
|
"model": self.model,
|
|
"created_at": 0,
|
|
"updated_at": 0,
|
|
"pinned": False,
|
|
"archived": False,
|
|
"project_id": None,
|
|
"profile": self.profile,
|
|
"input_tokens": self.input_tokens,
|
|
"output_tokens": self.output_tokens,
|
|
"estimated_cost": self.estimated_cost,
|
|
"personality": self.personality,
|
|
}
|
|
|
|
class FakeSessionDB:
|
|
def __init__(self, db_path=None):
|
|
self.db_path = db_path
|
|
|
|
def close(self):
|
|
return None
|
|
|
|
class CapturingAgent:
|
|
def __init__(self, **kwargs):
|
|
self.session_db = kwargs.get("session_db")
|
|
self._session_db = self.session_db
|
|
self.session_prompt_tokens = 0
|
|
self.session_completion_tokens = 0
|
|
self.session_estimated_cost_usd = None
|
|
self.context_compressor = None
|
|
self._last_error = None
|
|
self.ephemeral_system_prompt = None
|
|
|
|
def run_conversation(self, **kwargs):
|
|
history = list(kwargs.get("conversation_history") or [])
|
|
return {
|
|
"messages": history
|
|
+ [
|
|
{"role": "user", "content": kwargs.get("persist_user_message", "")},
|
|
{"role": "assistant", "content": "ok"},
|
|
]
|
|
}
|
|
|
|
def interrupt(self, _message):
|
|
return None
|
|
|
|
session_db_instances = []
|
|
|
|
def fake_session_db(db_path=None):
|
|
db = FakeSessionDB(db_path=db_path)
|
|
session_db_instances.append(db)
|
|
return db
|
|
|
|
fake_runtime_module = types.ModuleType("hermes_cli.runtime_provider")
|
|
fake_runtime_module.resolve_runtime_provider = lambda requested=None: {
|
|
"provider": requested or "test-provider",
|
|
"api_key": "synthetic-key",
|
|
"base_url": None,
|
|
}
|
|
fake_hermes_cli = types.ModuleType("hermes_cli")
|
|
fake_hermes_cli.runtime_provider = fake_runtime_module
|
|
fake_hermes_state = types.ModuleType("hermes_state")
|
|
fake_hermes_state.SessionDB = fake_session_db
|
|
|
|
fake_session = FakeSession()
|
|
|
|
monkeypatch.setattr(streaming, "get_session", lambda _sid: fake_session)
|
|
monkeypatch.setattr(streaming, "_get_ai_agent", lambda: CapturingAgent)
|
|
monkeypatch.setattr(
|
|
streaming,
|
|
"resolve_model_provider",
|
|
lambda _model: ("test-model", "test-provider", None),
|
|
)
|
|
monkeypatch.setattr(streaming, "_maybe_schedule_title_refresh", lambda *args, **kwargs: None)
|
|
monkeypatch.setattr(profiles, "get_hermes_home_for_profile", lambda _profile: profile_home)
|
|
monkeypatch.setattr(profiles, "get_profile_runtime_env", lambda _home: {})
|
|
monkeypatch.setattr(
|
|
oauth,
|
|
"resolve_runtime_provider_with_anthropic_env_lock",
|
|
lambda _resolver, requested=None: {
|
|
"provider": requested or "test-provider",
|
|
"api_key": "synthetic-key",
|
|
"base_url": None,
|
|
},
|
|
)
|
|
monkeypatch.setattr("api.config.get_config", lambda: {})
|
|
monkeypatch.setattr("api.config._resolve_cli_toolsets", lambda _cfg: [])
|
|
monkeypatch.setattr("api.config.load_settings", lambda: {})
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", fake_hermes_cli)
|
|
monkeypatch.setitem(sys.modules, "hermes_cli.runtime_provider", fake_runtime_module)
|
|
monkeypatch.setitem(sys.modules, "hermes_state", fake_hermes_state)
|
|
|
|
with cfg.SESSION_AGENT_CACHE_LOCK:
|
|
cfg.SESSION_AGENT_CACHE.clear()
|
|
streaming.STREAMS.clear()
|
|
streaming.CANCEL_FLAGS.clear()
|
|
streaming.AGENT_INSTANCES.clear()
|
|
streaming.STREAM_PARTIAL_TEXT.clear()
|
|
streaming.STREAM_REASONING_TEXT.clear()
|
|
streaming.STREAM_LIVE_TOOL_CALLS.clear()
|
|
|
|
streaming.STREAMS[fake_session.active_stream_id] = queue.Queue()
|
|
old_home = os.environ.get("HERMES_HOME")
|
|
try:
|
|
streaming._run_agent_streaming(
|
|
session_id=fake_session.session_id,
|
|
msg_text="hello from zhangtingban",
|
|
model="test-model",
|
|
model_provider="test-provider",
|
|
workspace=str(tmp_path),
|
|
stream_id=fake_session.active_stream_id,
|
|
)
|
|
finally:
|
|
if old_home is None:
|
|
os.environ.pop("HERMES_HOME", None)
|
|
else:
|
|
os.environ["HERMES_HOME"] = old_home
|
|
|
|
assert session_db_instances, "streaming should construct a SessionDB for session_search"
|
|
assert session_db_instances[0].db_path == profile_home / "state.db"
|
|
with cfg.SESSION_AGENT_CACHE_LOCK:
|
|
agent = cfg.SESSION_AGENT_CACHE[fake_session.session_id][0]
|
|
assert agent._session_db.db_path == profile_home / "state.db"
|