| import pytest |
| from unittest.mock import AsyncMock, MagicMock |
| from ankigen.llm_interface import OpenAIClientManager, OpenAIRateLimiter |
| from openai import OpenAIError |
|
|
| |
|
|
|
|
| @pytest.mark.anyio |
| async def test_initialize_client_valid_key(mocker): |
| manager = OpenAIClientManager() |
| |
| mock_openai_class = mocker.patch("ankigen.llm_interface.AsyncOpenAI", autospec=True) |
|
|
| await manager.initialize_client("sk-valid-key") |
|
|
| assert manager._api_key == "sk-valid-key" |
| assert manager._client is not None |
| mock_openai_class.assert_called_once_with(api_key="sk-valid-key") |
|
|
|
|
| @pytest.mark.anyio |
| async def test_initialize_client_invalid_key(): |
| manager = OpenAIClientManager() |
| |
| with pytest.raises(ValueError, match="Invalid OpenAI API key format."): |
| await manager.initialize_client("invalid-key") |
| assert manager._client is None |
|
|
|
|
| @pytest.mark.anyio |
| async def test_initialize_client_empty_key(): |
| manager = OpenAIClientManager() |
| with pytest.raises(ValueError, match="Invalid OpenAI API key format."): |
| await manager.initialize_client("") |
| assert manager._client is None |
|
|
|
|
| @pytest.mark.anyio |
| async def test_initialize_client_openai_error(mocker): |
| manager = OpenAIClientManager() |
| mocker.patch( |
| "ankigen.llm_interface.AsyncOpenAI", side_effect=OpenAIError("API Error") |
| ) |
|
|
| with pytest.raises(OpenAIError): |
| await manager.initialize_client("sk-error") |
|
|
| assert manager._client is None |
|
|
|
|
| @pytest.mark.anyio |
| async def test_initialize_client_unexpected_error(mocker): |
| manager = OpenAIClientManager() |
| mocker.patch( |
| "ankigen.llm_interface.AsyncOpenAI", side_effect=Exception("Unexpected") |
| ) |
|
|
| with pytest.raises( |
| RuntimeError, match="Unexpected error initializing AsyncOpenAI client." |
| ): |
| await manager.initialize_client("sk-unexpected") |
|
|
| assert manager._client is None |
|
|
|
|
| def test_get_client_not_initialized(): |
| manager = OpenAIClientManager() |
| with pytest.raises(RuntimeError, match="AsyncOpenAI client is not initialized"): |
| manager.get_client() |
|
|
|
|
| @pytest.mark.anyio |
| async def test_get_client_initialized(mocker): |
| manager = OpenAIClientManager() |
| mocker.patch("ankigen.llm_interface.AsyncOpenAI") |
| await manager.initialize_client("sk-valid") |
|
|
| client = manager.get_client() |
| assert client is not None |
|
|
|
|
| def test_context_manager_sync(mocker): |
| manager = OpenAIClientManager() |
| mock_close = mocker.patch.object(manager, "close") |
|
|
| with manager as m: |
| assert m is manager |
|
|
| mock_close.assert_called_once() |
|
|
|
|
| @pytest.mark.anyio |
| async def test_context_manager_async(mocker): |
| manager = OpenAIClientManager() |
| mock_aclose = mocker.patch.object(manager, "aclose", new_callable=AsyncMock) |
|
|
| async with manager as m: |
| assert m is manager |
|
|
| mock_aclose.assert_called_once() |
|
|
|
|
| def test_close_method(mocker): |
| manager = OpenAIClientManager() |
| mock_client = MagicMock() |
| |
| mock_client.close = MagicMock() |
| manager._client = mock_client |
|
|
| manager.close() |
|
|
| mock_client.close.assert_called_once() |
| assert manager._client is None |
|
|
|
|
| @pytest.mark.anyio |
| async def test_aclose_method(mocker): |
| manager = OpenAIClientManager() |
| mock_client = AsyncMock() |
| |
| mock_client.aclose = AsyncMock() |
| manager._client = mock_client |
|
|
| await manager.aclose() |
|
|
| mock_client.aclose.assert_called_once() |
| assert manager._client is None |
|
|
|
|
| @pytest.mark.anyio |
| async def test_aclose_method_fallback_to_sync(mocker): |
| manager = OpenAIClientManager() |
| |
| mock_client = MagicMock(spec_set=["close"]) |
| mock_client.close = MagicMock() |
| manager._client = mock_client |
|
|
| await manager.aclose() |
|
|
| mock_client.close.assert_called_once() |
| assert manager._client is None |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.anyio |
| async def test_rate_limiter_within_limits(): |
| limiter = OpenAIRateLimiter(tokens_per_minute=100) |
| |
| await limiter.wait_if_needed(50) |
| assert limiter.tokens_used_current_window == 50 |
|
|
| await limiter.wait_if_needed(40) |
| assert limiter.tokens_used_current_window == 90 |
|
|
|
|
| @pytest.mark.anyio |
| async def test_rate_limiter_window_reset(mocker): |
| |
| mock_monotonic = mocker.patch("time.monotonic") |
| mock_monotonic.return_value = 0.0 |
|
|
| limiter = OpenAIRateLimiter(tokens_per_minute=100) |
| await limiter.wait_if_needed(80) |
| assert limiter.tokens_used_current_window == 80 |
|
|
| |
| mock_monotonic.return_value = 61.0 |
|
|
| await limiter.wait_if_needed(10) |
|
|
| |
| assert limiter.tokens_used_current_window == 10 |
| assert limiter.current_window_start_time == 61.0 |
|
|
|
|
| @pytest.mark.anyio |
| async def test_rate_limiter_wait_needed(mocker): |
| mock_sleep = mocker.patch("asyncio.sleep", new_callable=AsyncMock) |
| mock_monotonic = mocker.patch("time.monotonic") |
|
|
| |
| mock_monotonic.return_value = 0.0 |
| limiter = OpenAIRateLimiter(tokens_per_minute=100) |
|
|
| |
| await limiter.wait_if_needed(90) |
| assert limiter.tokens_used_current_window == 90 |
|
|
| |
| |
| |
|
|
| |
| |
| |
| async def side_effect_sleep(delay): |
| mock_monotonic.return_value = 60.0 |
|
|
| mock_sleep.side_effect = side_effect_sleep |
|
|
| await limiter.wait_if_needed(20) |
|
|
| mock_sleep.assert_called_once_with(60.0) |
| assert limiter.tokens_used_current_window == 20 |
| assert limiter.current_window_start_time == 60.0 |
|
|