mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""Tests for SessionDB.get_anchored_view — anchored window + session bookends.
|
|
|
|
Used by the discovery shape of session_search: an FTS5 match becomes the
|
|
anchor, the call returns goal (bookend_start) + match (window) + resolution
|
|
(bookend_end) in a single round trip, no LLM.
|
|
"""
|
|
import pytest
|
|
|
|
from hermes_state import SessionDB
|
|
|
|
|
|
@pytest.fixture
|
|
def db(tmp_path):
|
|
return SessionDB(tmp_path / "state.db")
|
|
|
|
|
|
def _seed_long_session(db, sid="s1", n=30):
|
|
"""Create a long session with alternating user/assistant prose. Returns ids ascending."""
|
|
db.create_session(sid, source="cli")
|
|
ids = []
|
|
for i in range(n):
|
|
role = "user" if i % 2 == 0 else "assistant"
|
|
mid = db.append_message(sid, role=role, content=f"prose msg {i}")
|
|
ids.append(mid)
|
|
return ids
|
|
|
|
|
|
class TestWindowAndBookendShape:
|
|
def test_returns_window_with_bookend_start_and_end(self, db):
|
|
ids = _seed_long_session(db, n=30)
|
|
# Anchor mid-session
|
|
anchor = ids[15]
|
|
view = db.get_anchored_view("s1", anchor, window=3, bookend=3)
|
|
assert len(view["window"]) == 7 # ±3 + anchor
|
|
assert len(view["bookend_start"]) == 3
|
|
assert len(view["bookend_end"]) == 3
|
|
# bookend_start is the first 3 ids of the session
|
|
assert [m["id"] for m in view["bookend_start"]] == ids[:3]
|
|
# bookend_end is the last 3 ids of the session
|
|
assert [m["id"] for m in view["bookend_end"]] == ids[-3:]
|
|
|
|
def test_window_anchor_marked_correctly(self, db):
|
|
ids = _seed_long_session(db, n=20)
|
|
anchor = ids[10]
|
|
view = db.get_anchored_view("s1", anchor, window=2, bookend=3)
|
|
# Anchor message is present in the window
|
|
anchor_msgs = [m for m in view["window"] if m["id"] == anchor]
|
|
assert len(anchor_msgs) == 1
|
|
|
|
|
|
|
|
|
|
class TestRoleFiltering:
|
|
def test_tool_role_filtered_from_window(self, db):
|
|
db.create_session("s1", source="cli")
|
|
user_ids = []
|
|
for i in range(5):
|
|
user_ids.append(db.append_message("s1", role="user", content=f"u{i}"))
|
|
db.append_message("s1", role="tool", content=f"tool output {i}", tool_name="x")
|
|
# Anchor on user message
|
|
view = db.get_anchored_view("s1", user_ids[2], window=5, bookend=0)
|
|
# No tool messages should appear in the window
|
|
roles = [m.get("role") for m in view["window"]]
|
|
assert "tool" not in roles
|
|
|
|
def test_anchor_preserved_even_when_tool_role(self, db):
|
|
db.create_session("s1", source="cli")
|
|
db.append_message("s1", role="user", content="ask")
|
|
tool_id = db.append_message("s1", role="tool", content="tool output", tool_name="x")
|
|
db.append_message("s1", role="user", content="follow-up")
|
|
# Anchor on the tool message — should still appear despite default filter
|
|
view = db.get_anchored_view("s1", tool_id, window=5, bookend=0)
|
|
ids_in_window = [m["id"] for m in view["window"]]
|
|
assert tool_id in ids_in_window
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSessionIsolation:
|
|
"""Bookends must not cross session boundaries."""
|
|
|
|
def test_bookends_only_from_anchor_session(self, db):
|
|
ids1 = _seed_long_session(db, sid="s1", n=20)
|
|
_seed_long_session(db, sid="s2", n=20)
|
|
view = db.get_anchored_view("s1", ids1[10], window=2, bookend=3)
|
|
# All bookend messages should have session_id = s1 (or session_id col)
|
|
for m in view["bookend_start"] + view["bookend_end"]:
|
|
assert m.get("session_id") == "s1"
|