"""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-10):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