Files
hermes-agent/tests/run_agent/test_session_reset_fix.py
Siddharth Balyan f3006ebef9 refactor(tests): re-architect tests + fix CI failures (#5946)
* refactor: re-architect tests to mirror the codebase

* Update tests.yml

* fix: add missing tool_error imports after registry refactor

* fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist

patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers,
causing test_code_execution tests to hit the Modal remote path.

* fix(tests): fix update_check and telegram xdist failures

- test_update_check: replace patch("hermes_cli.banner.os.getenv") with
  monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os
  directly, it uses get_hermes_home() from hermes_constants.

- test_telegram_conflict/approval_buttons: provide real exception classes
  for telegram.error mock (NetworkError, TimedOut, BadRequest) so the
  except clause in connect() doesn't fail with "catching classes that do
  not inherit from BaseException" when xdist pollutes sys.modules.

* fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock
2026-04-07 17:19:07 -07:00

122 lines
4.3 KiB
Python

"""Tests for session reset completeness (fixes #2635).
/clear and /new must not carry stale state into the next session.
Two fields were added after reset_session_state() was written and were
therefore never cleared:
- ContextCompressor._previous_summary
- AIAgent._user_turn_count
"""
import sys
import types
from pathlib import Path
import pytest
# Ensure repo root is importable
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
# Stub out optional heavy dependencies not installed in the test environment
sys.modules.setdefault("fire", types.SimpleNamespace(Fire=lambda *a, **k: None))
sys.modules.setdefault("firecrawl", types.SimpleNamespace(Firecrawl=object))
sys.modules.setdefault("fal_client", types.SimpleNamespace())
from run_agent import AIAgent
from agent.context_compressor import ContextCompressor
def _make_minimal_agent() -> AIAgent:
"""Return an AIAgent constructed with the absolute minimum args.
We pass dummy values that bypass network calls and filesystem access.
The object is never used to make API calls — only its attributes and
reset_session_state() are exercised.
"""
agent = AIAgent.__new__(AIAgent) # skip __init__ entirely
# Seed the exact attributes that reset_session_state() writes
agent.session_total_tokens = 0
agent.session_input_tokens = 0
agent.session_output_tokens = 0
agent.session_prompt_tokens = 0
agent.session_completion_tokens = 0
agent.session_cache_read_tokens = 0
agent.session_cache_write_tokens = 0
agent.session_reasoning_tokens = 0
agent.session_api_calls = 0
agent.session_estimated_cost_usd = 0.0
agent.session_cost_status = "unknown"
agent.session_cost_source = "none"
# The two fields under test
agent._user_turn_count = 0
agent.context_compressor = None # will be set per-test as needed
return agent
class TestResetSessionState:
"""reset_session_state() must clear ALL session-scoped state."""
def test_previous_summary_cleared_on_reset(self):
"""Compression summary from old session must not leak into new session."""
agent = _make_minimal_agent()
compressor = ContextCompressor.__new__(ContextCompressor)
compressor._previous_summary = "Old session summary about unrelated topic"
# Seed counter attributes that reset_session_state touches
compressor.last_prompt_tokens = 100
compressor.last_completion_tokens = 50
compressor.last_total_tokens = 150
compressor.compression_count = 3
compressor._context_probed = True
agent.context_compressor = compressor
agent.reset_session_state()
assert compressor._previous_summary is None, (
"_previous_summary must be None after reset; got: "
f"{compressor._previous_summary!r}"
)
def test_user_turn_count_cleared_on_reset(self):
"""Turn counter must reset to 0 on new session."""
agent = _make_minimal_agent()
agent._user_turn_count = 7 # simulates turns accumulated in previous session
agent.context_compressor = None
agent.reset_session_state()
assert agent._user_turn_count == 0, (
f"_user_turn_count must be 0 after reset; got: {agent._user_turn_count}"
)
def test_both_fields_cleared_together(self):
"""Both stale fields are cleared in a single reset_session_state() call."""
agent = _make_minimal_agent()
agent._user_turn_count = 3
compressor = ContextCompressor.__new__(ContextCompressor)
compressor._previous_summary = "Stale summary"
compressor.last_prompt_tokens = 0
compressor.last_completion_tokens = 0
compressor.last_total_tokens = 0
compressor.compression_count = 0
compressor._context_probed = False
agent.context_compressor = compressor
agent.reset_session_state()
assert agent._user_turn_count == 0
assert compressor._previous_summary is None
def test_reset_without_compressor_does_not_raise(self):
"""reset_session_state() must not raise when context_compressor is None."""
agent = _make_minimal_agent()
agent._user_turn_count = 2
agent.context_compressor = None
# Must not raise
agent.reset_session_state()
assert agent._user_turn_count == 0