fix(session): resolve default state DB path at call time

DEFAULT_DB_PATH in hermes_state.py is computed at import time, freezing
the developer's real ~/.hermes even when a test fixture (or runtime
profile switch) later redirects HERMES_HOME. Any default SessionDB() —
e.g. gateway SessionStore — then opened the real state.db.

Add _default_db_path(): resolves get_hermes_home() fresh at call time,
while a deliberately re-pointed DEFAULT_DB_PATH (the established
monkeypatch escape hatch) still wins via an import-time snapshot
comparison, preserving existing test behavior. SessionDB.__init__ and
session_search's requirement check now use the resolver; explicit
db_path arguments are untouched.

Reimplemented from PR #11875 by @JorkeyLiu (original diff predates the
hermes_state rewrite); regression test ported and modernized.
This commit is contained in:
Jorkey Liu 2026-07-29 17:54:51 -07:00 committed by Teknium
parent 3e54a366e7
commit 05afea65f4
4 changed files with 52 additions and 3 deletions

View file

@ -0,0 +1,2 @@
JorkeyLiu
# PR #11875 salvage

View file

@ -234,6 +234,30 @@ T = TypeVar("T")
DEFAULT_DB_PATH = get_hermes_home() / "state.db"
# Import-time snapshot used by _default_db_path() to detect a deliberately
# re-pointed DEFAULT_DB_PATH (tests monkeypatch the constant directly).
_IMPORT_DEFAULT_DB_PATH = DEFAULT_DB_PATH
def _default_db_path() -> Path:
"""Resolve the default state DB path at call time.
``DEFAULT_DB_PATH`` is computed when this module is first imported, which
freezes the developer's real ``~/.hermes`` even when a test fixture later
redirects ``HERMES_HOME`` importing this module during collection was
enough to point every default ``SessionDB()`` at the real state.db.
Precedence:
1. A deliberately re-pointed ``DEFAULT_DB_PATH`` (differs from the
import-time snapshot the established test escape hatch) wins.
2. Otherwise resolve ``get_hermes_home()`` fresh so a runtime
``HERMES_HOME`` redirect takes effect regardless of import order.
"""
if DEFAULT_DB_PATH != _IMPORT_DEFAULT_DB_PATH:
return DEFAULT_DB_PATH
return get_hermes_home() / "state.db"
# ---------------------------------------------------------------------------
# WAL-compatibility fallback
# ---------------------------------------------------------------------------
@ -1765,7 +1789,7 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
_IMPORT_MAX_TOTAL_BYTES = 25 * 1024 * 1024
def __init__(self, db_path: Path = None, read_only: bool = False):
self.db_path = db_path or DEFAULT_DB_PATH
self.db_path = db_path or _default_db_path()
self.read_only = read_only
self._lock = threading.Lock()

View file

@ -24,6 +24,29 @@ from gateway.config import GatewayConfig, Platform, SessionResetPolicy
from gateway.session import SessionEntry, SessionStore
def test_session_store_default_db_uses_runtime_hermes_home(tmp_path, monkeypatch):
"""SessionStore must honor runtime HERMES_HOME when opening the default DB.
Regression for the import-time DEFAULT_DB_PATH freeze: importing
hermes_state before a fixture redirected HERMES_HOME used to pin every
default SessionDB() at the developer's real ~/.hermes/state.db.
"""
config = GatewayConfig(default_reset_policy=SessionResetPolicy(mode="none"))
fake_home = tmp_path / "alt_hermes_home"
fake_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(fake_home))
with patch("gateway.session.SessionStore._ensure_loaded"):
store = SessionStore(sessions_dir=tmp_path / "sessions", config=config)
try:
assert store._db is not None
assert store._db.db_path == fake_home / "state.db"
finally:
if store._db is not None:
store._db.close()
def _make_store(tmp_path, max_age_days: int = 90, has_active_processes_fn=None):
"""Build a SessionStore bypassing SQLite/disk-load side effects."""
config = GatewayConfig(

View file

@ -954,8 +954,8 @@ def session_search(
def check_session_search_requirements() -> bool:
"""Requires the SQLite state database."""
try:
from hermes_state import DEFAULT_DB_PATH
return DEFAULT_DB_PATH.parent.exists()
from hermes_state import _default_db_path
return _default_db_path().parent.exists()
except ImportError:
return False