Files
webui/tests/test_sprint42.py
nesquena-hermes 04401787ec fix: inject SessionDB into AIAgent for WebUI sessions — enables session_search (#356)
* fix: inject SessionDB into AIAgent for WebUI sessions

session_search tool requires a SessionDB instance passed via the
session_db parameter. The CLI and gateway paths already do this,
but the WebUI streaming path was missing it, causing every
session_search call to return 'Session database not available'.

Initialize SessionDB before creating the AIAgent and pass it through.
Failure is non-fatal — a warning is printed and session_search
gracefully degrades.

* fix: inject SessionDB into AIAgent for WebUI sessions (enables session_search) (#356)

- api/streaming.py: initialize SessionDB() before AIAgent construction and
  pass session_db= kwarg so session_search works in WebUI sessions
- tests/test_sprint42.py: 7 new tests covering SessionDB injection, try/except
  guard, WARNING log, ordering, and AST lock-safety check
- CHANGELOG.md: v0.50.13 entry; 822 tests total (up from 815)

---------

Co-authored-by: 王昌旭 <wangchangxu@xiaohongshu.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 10:53:58 -07:00

108 lines
4.4 KiB
Python

"""
Sprint 42 Tests: SessionDB injection into AIAgent for WebUI sessions (PR #356).
Covers:
- streaming.py: SessionDB is initialized inside _run_agent_streaming (import present)
- streaming.py: try/except guards SessionDB init so failures are non-fatal
- streaming.py: session_db= kwarg is passed to AIAgent constructor
- streaming.py: SessionDB init failure prints a WARNING (not silently swallowed)
- streaming.py: SessionDB init is placed before AIAgent construction
"""
import ast
import pathlib
import re
import unittest
REPO_ROOT = pathlib.Path(__file__).parent.parent
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text()
class TestSessionDBInjection(unittest.TestCase):
"""Verify SessionDB is initialized and passed to AIAgent in streaming.py."""
def test_hermes_state_import_present(self):
"""SessionDB must be imported from hermes_state inside _run_agent_streaming."""
self.assertIn(
"from hermes_state import SessionDB",
STREAMING_PY,
"SessionDB import missing from streaming.py (PR #356)",
)
def test_session_db_kwarg_passed_to_agent(self):
"""session_db= must be passed to the AIAgent constructor call."""
self.assertIn(
"session_db=_session_db",
STREAMING_PY,
"session_db kwarg not passed to AIAgent (PR #356)",
)
def test_sessiondb_init_in_try_except(self):
"""SessionDB() init must be wrapped in try/except for non-fatal failure handling."""
# Check that the try/except pattern surrounding SessionDB() is present
pattern = r"try:\s*\n\s*from hermes_state import SessionDB\s*\n\s*_session_db\s*=\s*SessionDB\(\)"
self.assertRegex(
STREAMING_PY,
pattern,
"SessionDB() init must be inside a try block for non-fatal error handling (PR #356)",
)
def test_sessiondb_failure_logs_warning(self):
"""A failure initializing SessionDB must print a WARNING (not silently drop the error)."""
self.assertIn(
"WARNING: SessionDB init failed",
STREAMING_PY,
"SessionDB init failure must log a WARNING message (PR #356)",
)
def test_session_db_initialized_before_agent_construction(self):
"""SessionDB initialization must appear before the AIAgent(...) constructor call."""
db_pos = STREAMING_PY.find("from hermes_state import SessionDB")
agent_pos = STREAMING_PY.find("session_db=_session_db")
self.assertGreater(
agent_pos,
db_pos,
"SessionDB init must appear before AIAgent construction (PR #356)",
)
def test_session_db_default_is_none(self):
"""_session_db must be initialized to None before the try block (safe default)."""
# Pattern: _session_db = None followed (eventually) by the try/SessionDB block
pattern = r"_session_db\s*=\s*None\s*\n\s*try:"
self.assertRegex(
STREAMING_PY,
pattern,
"_session_db must default to None before try/except block (PR #356)",
)
class TestSessionDBAST(unittest.TestCase):
"""AST-level checks: verify the try/except is not inside _ENV_LOCK (deadlock guard)."""
def setUp(self):
self.tree = ast.parse(STREAMING_PY)
def test_sessiondb_try_not_inside_env_lock(self):
"""The try block that wraps SessionDB init must NOT be inside a 'with _ENV_LOCK:' block.
Putting a try/except inside _ENV_LOCK is the deadlock pattern caught by test_sprint34.
The SessionDB try/except is outside the lock scope, which is correct.
"""
# Find all 'with _ENV_LOCK:' nodes; check none of their bodies contain
# a Try node that also contains 'from hermes_state import SessionDB'
for node in ast.walk(self.tree):
if not isinstance(node, ast.With):
continue
names = [getattr(item.context_expr, "id", "") for item in node.items]
if "_ENV_LOCK" not in names:
continue
# Walk the with-body for Try nodes
for stmt in node.body:
if isinstance(stmt, ast.Try):
# Check if this try imports hermes_state
src = ast.unparse(stmt)
self.assertNotIn(
"hermes_state",
src,
"SessionDB try/except must NOT be inside _ENV_LOCK body (deadlock risk)",
)