Files
hermes-agent/tests/agent/test_memory_nudge_counter_hydration.py
T
teknium1 d10bb2ab6f test: make tests/ mirror the source tree; drop issue numbers from filenames
`scripts/run_tests.sh tests/<dir>/` is how a change gets its regression
coverage run, so a test filed under the wrong directory is a test nobody
runs when that code changes. Two kinds of drift had accumulated.

Parallel directories for one source package, folded into the mirror:
  tests/acp        -> tests/acp_adapter   (its __init__/conftest move with it)
  tests/cli        -> tests/hermes_cli    (prompt_toolkit fixture merged into
                                           hermes_cli/conftest.py)
  tests/run_agent  -> tests/agent         (backoff fixture becomes
                                           agent/conftest.py)
  tests/relay      -> tests/gateway/relay
  tests/state      -> tests/hermes_state

246 loose files at tests/ root, routed by the package they import/patch:
hermes_cli, hermes_state, agent, gateway, tools, plugins, tui_gateway, cron.
Installer and desktop-update script tests go to tests/scripts/{install,
desktop_update}/. 43 tests of root-level modules (batch_runner, utils,
hermes_constants, packaging) stay at the root.

Filenames drop their issue numbers (95 files: test_89315_x.py -> test_x.py);
the number stays in the module docstring where it has context.

Collisions: test_cli_skin_integration.py existed in both tests/ and tests/cli
with different subsets — merged into one (10 tests, all kept);
run_agent/test_pre_compress_memory_context.py -> agent/..._handoff.py;
tests/test_account_usage.py -> agent/test_account_usage_fetch.py;
tests/test_web_server.py -> hermes_cli/test_web_server_ws_ping.py.
Deleted: test_minisweagent_path.py (empty since PR #2804),
test_model_picker_scroll.py (tested a private copy of the logic, imported
nothing), test_process_loop_event_loop_warning.py (asserted asyncio behaviour,
imported nothing from Hermes).

Repo-root path arithmetic (Path(__file__).parents[N], dirname chains) is
bumped for the 202 files that changed depth and verified by evaluating every
such expression against the new location. classify_changes' desktop-updater
lane prefix, tests-os.yml's ignore glob and every in-tree path comment follow
the moves. tests/test_tests_tree_layout.py keeps the tree from drifting back.
2026-09-13 09:18:02 -07:00

122 lines
4.7 KiB
Python

"""Regression test for issue #22357 — gateway memory-nudge counter hydration.
The gateway creates a fresh AIAgent for each inbound message in several
common scenarios (cache miss, 1h idle eviction at gateway/run.py
_AGENT_CACHE_IDLE_TTL_SECS, config-signature mismatch, process restart).
A freshly built AIAgent has _turns_since_memory=0 and _user_turn_count=0.
Without hydration from conversation_history, the memory.nudge_interval
trigger (`_turns_since_memory >= _memory_nudge_interval`) can never be
reached: every turn looks like turn 1 to the counter, so a user can chat
for hours without ever seeing a "💾 Self-improvement review:" message.
This test pins the hydration behavior added at the top of run_conversation().
"""
from __future__ import annotations
def _make_minimal_agent():
"""Build the smallest object that can run the hydration block.
The hydration code only touches attributes — no I/O, no API calls.
We can just set up a SimpleNamespace-like object with the right fields
and call run_conversation's prelude logic via a thin wrapper.
The hydration block itself is straightforward enough that we test it
by replicating it inline against the same inputs — that's the only
way to test ~10 lines deep inside a 500+ line method without rewriting
the whole agent loop.
"""
def _run_hydration(conversation_history, memory_nudge_interval=10,
prior_turn_count=0, prior_turns_since_memory=0):
"""Replicate the hydration block from run_agent.py:11128-11150.
Keeping this in sync with the production code is a one-line job; the
block has no dependencies on anything except primitives + history.
"""
user_turn_count = prior_turn_count
turns_since_memory = prior_turns_since_memory
if conversation_history and user_turn_count == 0:
prior_user_turns = sum(
1 for m in conversation_history if m.get("role") == "user"
)
if prior_user_turns > 0:
user_turn_count = prior_user_turns
if memory_nudge_interval > 0 and turns_since_memory == 0:
turns_since_memory = prior_user_turns % memory_nudge_interval
return user_turn_count, turns_since_memory
def test_no_history_leaves_counters_at_zero():
user_turn, since_mem = _run_hydration([], memory_nudge_interval=10)
assert user_turn == 0
assert since_mem == 0
def test_seven_user_turns_history_hydrates_to_seven():
"""Mid-cycle history: 7 prior user turns, interval 10 → counter at 7."""
history = []
for i in range(7):
history.append({"role": "user", "content": f"q{i}"})
history.append({"role": "assistant", "content": f"a{i}"})
user_turn, since_mem = _run_hydration(history, memory_nudge_interval=10)
assert user_turn == 7
assert since_mem == 7 # 7 % 10 = 7, next 3 turns will trigger review
def test_idempotent_when_counters_already_set():
"""A cached agent with existing counters must NOT have them clobbered.
Without the `_user_turn_count == 0` guard, cached agents would lose
their accumulated state every time they re-entered the function.
"""
history = [{"role": "user", "content": "q1"}, {"role": "assistant", "content": "a1"}]
user_turn, since_mem = _run_hydration(
history, memory_nudge_interval=10,
prior_turn_count=15, prior_turns_since_memory=5,
)
# Existing counters preserved (cache hit case)
assert user_turn == 15
assert since_mem == 5
def test_production_code_contains_hydration_block():
"""Smoke test: confirm the hydration code is actually wired into the
turn path. If someone deletes it, tests above still pass against the
inline replica — this fails them awake.
The agent-loop prologue now lives in ``agent/turn_context.py``
(``build_turn_context``), with the loop body in
``agent/conversation_loop.py``. Assert the block is present in the
turn subsystem — if it disappears entirely, this guard fails loudly.
Either module counts so the guard tolerates legitimate relocation
within the turn subsystem.
"""
from pathlib import Path
repo = Path(__file__).resolve().parents[2]
turn_src = "".join(
(repo / "agent" / name).read_text(encoding="utf-8")
for name in ("conversation_loop.py", "turn_context.py")
)
# Anchor on the unique comment + the modulo line.
assert "Hydrate per-session nudge counters from persisted history" in turn_src, (
"Hydration comment missing from the turn subsystem "
"(conversation_loop.py / turn_context.py)"
)
assert (
"agent._turns_since_memory = prior_user_turns % agent._memory_nudge_interval"
in turn_src
), "Hydration modulo assignment missing from the turn subsystem"