hermes-agent/tests/test_trajectory_compressor_async.py
Teknium 0dd26c9495
fix(tests): fix 78 CI test failures and remove dead test (#9036)
Production fixes:
- voice_mode.py: add is_recording property to AudioRecorder (parity with TermuxAudioRecorder)
- cronjob_tools.py: add sms example to deliver description

Test fixes:
- test_real_interrupt_subagent: add missing _execution_thread_id (fixes 19 cascading failures from leaked _build_system_prompt patch)
- test_anthropic_error_handling: add _FakeMessages, override _interruptible_streaming_api_call (6 fixes)
- test_ctx_halving_fix: add missing request_overrides attribute (4 fixes)
- test_context_token_tracking: set _disable_streaming=True for non-streaming test path (4 fixes)
- test_dict_tool_call_args: set _disable_streaming=True (1 fix)
- test_provider_parity: add model='gpt-4o' for AIGateway tests to meet 64K minimum context (4 fixes)
- test_session_race_guard: add user_id to SessionSource (5 fixes)
- test_restart_drain/helpers: add user_id to SessionSource (2 fixes)
- test_telegram_photo_interrupts: add user_id to SessionSource
- test_interrupt: target thread_id for per-thread interrupt system (2 fixes)
- test_zombie_process_cleanup: rewrite with object.__new__ for refactored GatewayRunner.stop() (1 fix)
- test_browser_camofox_state: update config version 15->17 (1 fix)
- test_trajectory_compressor_async: widen lookback window 10->20 for line-shifted AsyncOpenAI (1 fix)
- test_voice_mode: fixed by production is_recording addition (5 fixes)
- test_voice_cli_integration: add _attached_images to CLI stub (2 fixes)
- test_hermes_logging: explicit propagation/level reset for cross-test pollution defense (1 fix)
- test_run_agent: add base_url for OpenRouter detection tests (2 fixes)

Deleted:
- test_inline_think_blocks_reasoning_only_accepted: tested unimplemented inline <think> handling
2026-04-13 10:50:24 -07:00

115 lines
4.4 KiB
Python

"""Tests for trajectory_compressor AsyncOpenAI event loop binding.
The AsyncOpenAI client was created once at __init__ time and stored as an
instance attribute. When process_directory() calls asyncio.run() — which
creates and closes a fresh event loop — the client's internal httpx
transport remains bound to the now-closed loop. A second call to
process_directory() would fail with "Event loop is closed".
The fix creates the AsyncOpenAI client lazily via _get_async_client() so
each asyncio.run() gets a client bound to the current loop.
"""
import types
from unittest.mock import MagicMock, patch
import pytest
class TestAsyncClientLazyCreation:
"""trajectory_compressor.py — _get_async_client()"""
def test_async_client_none_after_init(self):
"""async_client should be None after __init__ (not eagerly created)."""
from trajectory_compressor import TrajectoryCompressor
comp = TrajectoryCompressor.__new__(TrajectoryCompressor)
comp.config = MagicMock()
comp.config.base_url = "https://api.example.com/v1"
comp.config.api_key_env = "TEST_API_KEY"
comp._use_call_llm = False
comp.async_client = None
comp._async_client_api_key = "test-key"
assert comp.async_client is None
def test_get_async_client_creates_new_client(self):
"""_get_async_client() should create a fresh AsyncOpenAI instance."""
from trajectory_compressor import TrajectoryCompressor
comp = TrajectoryCompressor.__new__(TrajectoryCompressor)
comp.config = MagicMock()
comp.config.base_url = "https://api.example.com/v1"
comp._async_client_api_key = "test-key"
comp.async_client = None
mock_async_openai = MagicMock()
with patch("openai.AsyncOpenAI", mock_async_openai):
client = comp._get_async_client()
mock_async_openai.assert_called_once_with(
api_key="test-key",
base_url="https://api.example.com/v1",
)
assert comp.async_client is not None
def test_get_async_client_creates_fresh_each_call(self):
"""Each call to _get_async_client() creates a NEW client instance,
so it binds to the current event loop."""
from trajectory_compressor import TrajectoryCompressor
comp = TrajectoryCompressor.__new__(TrajectoryCompressor)
comp.config = MagicMock()
comp.config.base_url = "https://api.example.com/v1"
comp._async_client_api_key = "test-key"
comp.async_client = None
call_count = 0
instances = []
def mock_constructor(**kwargs):
nonlocal call_count
call_count += 1
instance = MagicMock()
instances.append(instance)
return instance
with patch("openai.AsyncOpenAI", side_effect=mock_constructor):
client1 = comp._get_async_client()
client2 = comp._get_async_client()
# Should have created two separate instances
assert call_count == 2
assert instances[0] is not instances[1]
class TestSourceLineVerification:
"""Verify the actual source has the lazy pattern applied."""
@staticmethod
def _read_file() -> str:
import os
base = os.path.dirname(os.path.dirname(__file__))
with open(os.path.join(base, "trajectory_compressor.py")) as f:
return f.read()
def test_no_eager_async_openai_in_init(self):
"""__init__ should NOT create AsyncOpenAI eagerly."""
src = self._read_file()
# The old pattern: self.async_client = AsyncOpenAI(...) in _init_summarizer
# should not exist — only self.async_client = None
lines = src.split("\n")
for i, line in enumerate(lines, 1):
if "self.async_client = AsyncOpenAI(" in line and "_get_async_client" not in lines[max(0,i-3):i+1]:
# Allow it inside _get_async_client method
# Check if we're inside _get_async_client by looking at context
context = "\n".join(lines[max(0,i-20):i+1])
if "_get_async_client" not in context:
pytest.fail(
f"Line {i}: AsyncOpenAI created eagerly outside _get_async_client()"
)
def test_get_async_client_method_exists(self):
"""_get_async_client method should exist."""
src = self._read_file()
assert "def _get_async_client(self)" in src