145 lines
6.1 KiB
Python
145 lines
6.1 KiB
Python
"""Regression tests: session continuity when the default/global state.db is unavailable.
|
|
|
|
Root cause (live incident, 2026-08-17): when the default/global state.db is
|
|
corrupt or unreadable at gateway startup, ``SessionStore._db`` degrades to
|
|
None (JSONL routing fallback) and ``GatewayRunner._session_db`` is None.
|
|
Under multiplexed profile routes the AIAgent lazily opens the PROFILE's
|
|
state.db (``AIAgent._get_session_db_for_recall``) and its
|
|
``_ensure_db_session`` created the session row WITHOUT routing identity
|
|
(session_key/chat_id/chat_type/thread_id/user_id/origin_json/display_name
|
|
all NULL), so the row was unrecoverable by
|
|
``find_latest_gateway_session_for_peer``. The gateway's own
|
|
``record_gateway_session_peer`` self-heal never ran because ``_db`` was None.
|
|
On the next message, ``SessionStore.load_transcript`` returned [] even though
|
|
the transcript existed in the profile DB -> the agent lost prior context.
|
|
|
|
These tests pin fix 1 (routing identity on lazy row creation):
|
|
``AIAgent._ensure_db_session`` persists the full routing identity the
|
|
agent already carries, so the lazily-created row is recoverable. (The
|
|
transcript-recovery half of the original report is covered separately by
|
|
the scope-aware session DB resolution on main.)
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
SESSION_ID = "degraded-db-continuity-session"
|
|
CHAT_ID = "-1001234567890"
|
|
THREAD_ID = "42"
|
|
SESSION_KEY = "agent:orion:telegram:group:-1001234567890:8148316720"
|
|
|
|
|
|
def _make_gateway_agent(session_db):
|
|
"""Build an AIAgent carrying gateway routing identity (multiplex profile route)."""
|
|
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}):
|
|
from run_agent import AIAgent
|
|
|
|
agent = AIAgent(
|
|
api_key="test-key",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
model="test/model",
|
|
quiet_mode=True,
|
|
session_db=session_db,
|
|
session_id=SESSION_ID,
|
|
skip_context_files=True,
|
|
skip_memory=True,
|
|
platform="telegram",
|
|
user_id="8148316720",
|
|
user_name="testuser",
|
|
chat_id=CHAT_ID,
|
|
chat_name="Test Group",
|
|
chat_type="group",
|
|
thread_id=THREAD_ID,
|
|
gateway_session_key=SESSION_KEY,
|
|
)
|
|
# The multiplexed profile scope would resolve the active profile name to
|
|
# the route's profile (e.g. "orion"); pin it so the row + origin_json
|
|
# carry the profile deterministically.
|
|
with patch("hermes_cli.profiles.get_active_profile_name", return_value="orion"):
|
|
agent._ensure_db_session()
|
|
return agent
|
|
|
|
|
|
class TestEnsureDbSessionPersistsRoutingIdentity:
|
|
def test_lazy_creation_writes_session_key_and_chat_identity(self):
|
|
"""First-created row must carry gateway routing metadata (not identity-less)."""
|
|
from hermes_state import SessionDB
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db = SessionDB(db_path=Path(tmpdir) / "t.db")
|
|
try:
|
|
_make_gateway_agent(db)
|
|
row = db.get_session(SESSION_ID)
|
|
assert row is not None
|
|
assert row["session_key"] == SESSION_KEY
|
|
assert row["chat_id"] == CHAT_ID
|
|
assert row["chat_type"] == "group"
|
|
assert row["thread_id"] == THREAD_ID
|
|
assert row["user_id"] == "8148316720"
|
|
assert row["display_name"] == "Test Group"
|
|
assert row["source"] == "telegram"
|
|
assert row["profile_name"] == "orion"
|
|
origin = json.loads(row["origin_json"])
|
|
assert origin["platform"] == "telegram"
|
|
assert origin["chat_id"] == CHAT_ID
|
|
assert origin["chat_name"] == "Test Group"
|
|
assert origin["chat_type"] == "group"
|
|
assert origin["user_id"] == "8148316720"
|
|
assert origin["user_name"] == "testuser"
|
|
assert origin["thread_id"] == THREAD_ID
|
|
assert origin["profile"] == "orion"
|
|
finally:
|
|
db.close()
|
|
|
|
def test_row_recoverable_by_peer_lookup(self):
|
|
"""find_latest_gateway_session_for_peer must find the lazily-created row."""
|
|
from hermes_state import SessionDB
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db = SessionDB(db_path=Path(tmpdir) / "t.db")
|
|
try:
|
|
_make_gateway_agent(db)
|
|
# The peer lookup keys on session_key + source; the regression
|
|
# left session_key NULL so this returned None on every restart.
|
|
found = db.find_latest_gateway_session_for_peer(
|
|
session_key=SESSION_KEY,
|
|
source="telegram",
|
|
)
|
|
assert found is not None
|
|
assert found["id"] == SESSION_ID
|
|
finally:
|
|
db.close()
|
|
|
|
def test_cli_session_stays_identity_less(self):
|
|
"""A plain CLI agent (no gateway fields) keeps the old shape — no origin_json."""
|
|
from hermes_state import SessionDB
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db = SessionDB(db_path=Path(tmpdir) / "t.db")
|
|
try:
|
|
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}):
|
|
from run_agent import AIAgent
|
|
|
|
agent = AIAgent(
|
|
api_key="test-key",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
model="test/model",
|
|
quiet_mode=True,
|
|
session_db=db,
|
|
session_id=SESSION_ID + "-cli",
|
|
skip_context_files=True,
|
|
skip_memory=True,
|
|
)
|
|
agent._ensure_db_session()
|
|
row = db.get_session(SESSION_ID + "-cli")
|
|
assert row is not None
|
|
assert row["session_key"] is None
|
|
assert row["chat_id"] is None
|
|
assert row["origin_json"] is None
|
|
finally:
|
|
db.close()
|