mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Systematic prune per AGENTS.md test policy, one pass over every major test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli, cron, tui_gateway, honcho/openviking, root-level): - DELETE: source-reading tests (read_text/getsource on prod files), change-detector tests (exact catalog counts, model-name snapshots, config version literals), mock-echo tests (assert a mock returns what it was told), assertion-free/trivial tests, near-duplicate parametrizations (boundaries + one representative kept), async/sync twin duplicates, cosmetic within-file variations. - KEEP (mandatory): security/redaction/approval guards, message-role alternation invariants, prompt-caching/deterministic-call-id invariants, issue-number regression tests (deduped), E2E tests. - 6 test files deleted outright (script-style/no-assert or fully redundant); conftest.py, fakes/, fixtures/ untouched. - tests/acp/conftest.py added: autouse fixture stubs the live models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server tests performed on every session create — test_server.py 147s → 3.4s, and the tests are now genuinely hermetic. - Sleep-based slowness shrunk where safe (codex_ttfb_watchdog, compression_concurrent_fork, etc.); no wall-clock assertion tightened. Verification: full hermetic suite via scripts/run_tests.sh — 2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall (baseline: 583s wall, 13,564s subprocess CPU).
131 lines
4.2 KiB
Python
131 lines
4.2 KiB
Python
"""Tests for the SessionDB read-path split (per-thread read-only connections).
|
|
|
|
The gateway shares ONE SessionDB across every agent, so recall/browse reads
|
|
used to queue behind writer flushes on self._lock — a measured production
|
|
convoy (a 0.2s FTS query stretched to 112s while 6-8 concurrent turns
|
|
flushed tool results). These tests pin the new contract: reads run on a
|
|
per-thread read-only connection under WAL, never touch self._lock, and fall
|
|
back to the legacy locked path when WAL or the read connection is missing.
|
|
"""
|
|
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
from hermes_state import SessionDB
|
|
|
|
|
|
@pytest.fixture()
|
|
def db(tmp_path):
|
|
d = SessionDB(db_path=tmp_path / "state.db")
|
|
d.create_session(session_id="s1", source="cli", model="m")
|
|
d.append_message("s1", role="user", content="hello graphiti world")
|
|
d.append_message("s1", role="assistant", content="the neo4j daemon is healthy")
|
|
yield d
|
|
d.close()
|
|
|
|
|
|
@pytest.mark.requires_wal
|
|
def test_read_conn_is_per_thread(db):
|
|
conns = {}
|
|
|
|
def grab(key):
|
|
conns[key] = db._get_read_conn()
|
|
|
|
t1 = threading.Thread(target=grab, args=(1,))
|
|
t2 = threading.Thread(target=grab, args=(2,))
|
|
t1.start(); t2.start(); t1.join(); t2.join()
|
|
assert conns[1] is not None and conns[2] is not None
|
|
assert conns[1] is not conns[2]
|
|
|
|
|
|
def test_read_conn_reused_within_thread(db):
|
|
assert db._get_read_conn() is db._get_read_conn()
|
|
|
|
|
|
@pytest.mark.requires_wal
|
|
def test_reads_do_not_take_writer_lock(db):
|
|
"""Reads must complete while another thread holds self._lock."""
|
|
acquired = db._lock.acquire()
|
|
assert acquired
|
|
try:
|
|
done = {}
|
|
|
|
def reader():
|
|
done["session"] = db.get_session("s1")
|
|
done["search"] = db.search_messages("graphiti", limit=10)
|
|
done["messages"] = db.get_messages("s1")
|
|
|
|
t = threading.Thread(target=reader)
|
|
t.start()
|
|
t.join(timeout=5.0)
|
|
assert not t.is_alive(), "read path blocked on writer lock"
|
|
assert done["session"]["id"] == "s1"
|
|
assert any("graphiti" in (m.get("snippet") or "") for m in done["search"])
|
|
assert len(done["messages"]) == 2
|
|
finally:
|
|
db._lock.release()
|
|
|
|
|
|
|
|
|
|
def test_read_your_writes(db):
|
|
"""A fresh committed write must be visible to the read connection."""
|
|
db.append_message("s1", role="user", content="zanzibar checkpoint")
|
|
rows = db.search_messages("zanzibar", limit=5)
|
|
assert rows, "committed write invisible to read connection"
|
|
|
|
|
|
|
|
|
|
def test_non_wal_uses_locked_path(db):
|
|
db._wal_active = False
|
|
assert db._get_read_conn() is None
|
|
# And queries still work via the legacy path.
|
|
assert db.get_session("s1")["id"] == "s1"
|
|
|
|
|
|
@pytest.mark.requires_wal
|
|
def test_read_conn_open_failure_marks_thread(db, monkeypatch, tmp_path):
|
|
"""A failed read-conn open must not retry per query; fallback still works."""
|
|
import sqlite3 as _sqlite3
|
|
|
|
calls = {"n": 0}
|
|
real_connect = _sqlite3.connect
|
|
|
|
def failing_connect(*a, **k):
|
|
if a and isinstance(a[0], str) and a[0].startswith("file:") and "mode=ro" in a[0]:
|
|
calls["n"] += 1
|
|
raise _sqlite3.OperationalError("simulated open failure")
|
|
return real_connect(*a, **k)
|
|
|
|
fresh = SessionDB(db_path=tmp_path / "state2.db")
|
|
try:
|
|
fresh.create_session(session_id="x", source="cli", model="m")
|
|
monkeypatch.setattr("hermes_state.sqlite3.connect", failing_connect)
|
|
assert fresh.get_session("x")["id"] == "x"
|
|
assert fresh.get_session("x")["id"] == "x"
|
|
assert calls["n"] == 1, "open failure should be remembered per thread"
|
|
finally:
|
|
fresh.close()
|
|
|
|
|
|
@pytest.mark.requires_wal
|
|
def test_anchored_view_and_around_use_read_path(db):
|
|
msgs = db.get_messages("s1")
|
|
anchor = msgs[0]["id"]
|
|
acquired = db._lock.acquire()
|
|
try:
|
|
done = {}
|
|
|
|
def reader():
|
|
done["around"] = db.get_messages_around("s1", anchor, window=2)
|
|
done["view"] = db.get_anchored_view("s1", anchor, window=2, bookend=1)
|
|
|
|
t = threading.Thread(target=reader)
|
|
t.start(); t.join(timeout=5.0)
|
|
assert not t.is_alive(), "anchored reads blocked on writer lock"
|
|
assert done["around"]["window"]
|
|
assert done["view"]["window"]
|
|
finally:
|
|
db._lock.release()
|