Files
litellm/tests/test_litellm/containers/test_container_api.py
T
shin-bot-litellm df299d3193 fix(tests): Fix flaky container and scientific notation tests (#20650)
* fix(tests): Mock async_container_create_handler for async router test

The test was mocking container_create_handler (sync), but
router.acreate_container uses _is_async=True which calls
async_container_create_handler. This caused the test to hit
the real OpenAI API.

Fixed by using AsyncMock on async_container_create_handler.

* fix(tests): Use uuid for unique model name in scientific notation test

The test was using a static "unique" model name which could cause
conflicts when running tests in parallel (-n 16 in CI). Using uuid
ensures truly unique names to prevent test pollution.

---------

Co-authored-by: Shin <shin@openclaw.ai>
2026-02-07 09:57:08 -08:00

353 lines
13 KiB
Python

import asyncio
import json
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.containers.main import (
acreate_container,
adelete_container,
alist_containers,
aretrieve_container,
create_container,
delete_container,
list_containers,
retrieve_container,
)
from litellm.main import base_llm_http_handler
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
from litellm.llms.openai.containers.transformation import OpenAIContainerConfig
from litellm.router import Router
from litellm.types.containers.main import (
ContainerListResponse,
ContainerObject,
DeleteContainerResult,
)
@pytest.fixture(autouse=True)
def clear_client_cache():
"""
Clear the HTTP client cache before each test to ensure mocks are used.
This prevents cached real clients from being reused across tests.
"""
cache = getattr(litellm, "in_memory_llm_clients_cache", None)
if cache is not None:
cache.flush_cache()
yield
# Clear again after test to avoid polluting other tests
if cache is not None:
cache.flush_cache()
class TestContainerAPI:
"""Test suite for container API functionality."""
def test_create_container_basic(self):
"""Test basic container creation functionality."""
# Mock the container creation response
mock_response = ContainerObject(
id="cntr_123456",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Test Container"
)
with patch.object(base_llm_http_handler, 'container_create_handler', return_value=mock_response):
response = create_container(
name="Test Container",
custom_llm_provider="openai"
)
assert isinstance(response, ContainerObject)
assert response.id == "cntr_123456"
assert response.name == "Test Container"
assert response.status == "running"
assert response.object == "container"
def test_create_container_with_expires_after(self):
"""Test container creation with expires_after parameter."""
mock_response = ContainerObject(
id="cntr_789",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 30},
last_active_at=1747857508,
name="Expiring Container"
)
with patch.object(base_llm_http_handler, 'container_create_handler', return_value=mock_response):
response = create_container(
name="Expiring Container",
expires_after={"anchor": "last_active_at", "minutes": 30},
custom_llm_provider="openai"
)
assert response.expires_after.minutes == 30
assert response.expires_after.anchor == "last_active_at"
def test_create_container_with_file_ids(self):
"""Test container creation with file_ids parameter."""
mock_response = ContainerObject(
id="cntr_file_test",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Container with Files"
)
with patch.object(base_llm_http_handler, 'container_create_handler', return_value=mock_response):
response = create_container(
name="Container with Files",
file_ids=["file_123", "file_456"],
custom_llm_provider="openai"
)
assert response.name == "Container with Files"
@pytest.mark.asyncio
async def test_acreate_container_basic(self):
"""Test basic async container creation functionality."""
mock_response = ContainerObject(
id="cntr_async_123",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Async Test Container"
)
with patch.object(base_llm_http_handler, 'container_create_handler', return_value=mock_response):
response = await acreate_container(
name="Async Test Container",
custom_llm_provider="openai"
)
assert isinstance(response, ContainerObject)
assert response.id == "cntr_async_123"
assert response.name == "Async Test Container"
@pytest.mark.asyncio
async def test_alist_containers_basic(self):
"""Test basic async container listing functionality."""
mock_response = ContainerListResponse(
object="list",
data=[
ContainerObject(
id="cntr_async_list",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Async List Container"
)
],
first_id="cntr_async_list",
last_id="cntr_async_list",
has_more=False
)
with patch.object(base_llm_http_handler, 'container_list_handler', return_value=mock_response):
response = await alist_containers(
custom_llm_provider="openai"
)
assert isinstance(response, ContainerListResponse)
assert len(response.data) == 1
@pytest.mark.parametrize(
"container_id,container_name,status,provider",
[
("cntr_retrieve_test", "Retrieved Container", "running", "openai"),
("cntr_different_id", "Another Container", "stopped", "openai"),
],
)
def test_retrieve_container_basic(self, container_id, container_name, status, provider):
"""Test basic container retrieval functionality.
This test verifies that:
1. retrieve_container correctly calls the handler with the container_id
2. The response is properly deserialized into a ContainerObject
3. All fields are correctly mapped from the handler response
4. The function works with different container states and IDs
"""
# Arrange: Create mock response with test parameters
mock_response = ContainerObject(
id=container_id,
object="container",
created_at=1747857508,
status=status,
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name=container_name
)
with patch.object(base_llm_http_handler, 'container_retrieve_handler', return_value=mock_response) as mock_method:
# Act: Call retrieve_container
response = retrieve_container(
container_id=container_id,
custom_llm_provider=provider
)
# Assert: Verify the handler was called correctly
mock_method.assert_called_once()
call_kwargs = mock_method.call_args.kwargs
assert call_kwargs["container_id"] == container_id
# Assert: Verify response structure and content
assert isinstance(response, ContainerObject)
assert response.id == container_id
assert response.name == container_name
assert response.status == status
assert response.object == "container"
assert response.expires_after.minutes == 20
assert response.expires_after.anchor == "last_active_at"
@pytest.mark.asyncio
async def test_aretrieve_container_basic(self):
"""Test basic async container retrieval functionality."""
container_id = "cntr_async_retrieve"
mock_response = ContainerObject(
id=container_id,
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Async Retrieved Container"
)
with patch.object(base_llm_http_handler, 'container_retrieve_handler', return_value=mock_response):
response = await aretrieve_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert isinstance(response, ContainerObject)
assert response.id == container_id
def test_delete_container_basic(self):
"""Test basic container deletion functionality."""
container_id = "cntr_delete_test"
mock_response = DeleteContainerResult(
id=container_id,
object="container.deleted",
deleted=True
)
with patch.object(base_llm_http_handler, 'container_delete_handler', return_value=mock_response):
response = delete_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert isinstance(response, DeleteContainerResult)
assert response.id == container_id
assert response.deleted == True
assert response.object == "container.deleted"
@pytest.mark.asyncio
async def test_adelete_container_basic(self):
"""Test basic async container deletion functionality."""
container_id = "cntr_async_delete"
mock_response = DeleteContainerResult(
id=container_id,
object="container.deleted",
deleted=True
)
with patch.object(base_llm_http_handler, 'container_delete_handler', return_value=mock_response):
response = await adelete_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert isinstance(response, DeleteContainerResult)
assert response.id == container_id
assert response.deleted == True
def test_create_container_error_handling(self):
"""Test error handling in container creation."""
with patch.object(base_llm_http_handler, 'container_create_handler', side_effect=Exception("API Error")):
with pytest.raises(Exception):
create_container(
name="Error Test Container",
custom_llm_provider="openai"
)
def test_container_provider_config_retrieval(self):
"""Test that provider config is retrieved correctly."""
mock_response = ContainerObject(
id="cntr_config_test",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Config Test"
)
with patch('litellm.containers.main.ProviderConfigManager') as mock_config_manager:
mock_config_manager.get_provider_container_config.return_value = OpenAIContainerConfig()
with patch.object(base_llm_http_handler, 'container_create_handler', return_value=mock_response):
response = create_container(
name="Config Test",
custom_llm_provider="openai"
)
# Verify provider config was requested
mock_config_manager.get_provider_container_config.assert_called_once()
assert response.name == "Config Test"
@pytest.mark.asyncio
async def test_router_acreate_container_without_model(self):
"""
Test that router.acreate_container works without a model configured.
Ensures container operations bypass model deployment lookup.
"""
router = Router(model_list=[])
mock_response = ContainerObject(
id="cntr_test",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Test Container"
)
# Mock async_container_create_handler since router.acreate_container
# uses _is_async=True which calls the async handler
with patch.object(
base_llm_http_handler,
'async_container_create_handler',
new_callable=AsyncMock,
return_value=mock_response
):
result = await router.acreate_container(
name="Test Container",
custom_llm_provider="openai"
)
assert result.id == "cntr_test"
assert result.name == "Test Container"