# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Unit tests for the /v1/containers CRUD client methods. Covers: - list / create / delete all send ``OpenAI-Beta: containers=v1``. Without it, OpenAI silently no-ops the DELETE but still returns 200 ``{"deleted": true}``. - ``delete_openai_container`` raises when the body omits ``{"deleted": true}``, even on a 2xx response. """ from __future__ import annotations import asyncio import json import httpx import pytest from core.inference import external_provider as ep_mod from core.inference.external_provider import ExternalProviderClient def _drive(coro): return asyncio.new_event_loop().run_until_complete(coro) def _mock_http_client(monkeypatch, handler): """Wire `handler` for the shared `_http_client` AND any per-call `httpx.AsyncClient(...)`. delete_openai_container creates a fresh AsyncClient (see external_provider.delete_openai_container), so we must also intercept that constructor.""" transport = httpx.MockTransport(handler) monkeypatch.setattr(ep_mod, "_http_client", httpx.AsyncClient(transport = transport)) real_async_client = httpx.AsyncClient def _patched_async_client(*args, **kwargs): kwargs["transport"] = transport return real_async_client(*args, **kwargs) monkeypatch.setattr(ep_mod.httpx, "AsyncClient", _patched_async_client) def _make_client() -> ExternalProviderClient: return ExternalProviderClient( provider_type = "openai", base_url = "https://api.openai.com/v1", api_key = "sk-test", ) def test_list_sends_openai_beta_header(monkeypatch): seen: dict = {} def handler(request: httpx.Request) -> httpx.Response: seen["headers"] = dict(request.headers) seen["url"] = str(request.url) return httpx.Response( 200, json = {"data": [{"id": "cntr_x", "name": "auto"}]}, ) _mock_http_client(monkeypatch, handler) result = _drive(_make_client().list_openai_containers()) assert result == [{"id": "cntr_x", "name": "auto"}] assert seen["headers"].get("openai-beta") == "containers=v1" assert seen["url"] == "https://api.openai.com/v1/containers" def test_create_sends_openai_beta_header(monkeypatch): seen: dict = {} def handler(request: httpx.Request) -> httpx.Response: seen["headers"] = dict(request.headers) seen["body"] = json.loads(request.content.decode("utf-8")) return httpx.Response(200, json = {"id": "cntr_new", "name": "analysis"}) _mock_http_client(monkeypatch, handler) result = _drive(_make_client().create_openai_container(name = "analysis", ttl_minutes = 30)) assert result == {"id": "cntr_new", "name": "analysis"} assert seen["headers"].get("openai-beta") == "containers=v1" assert seen["body"]["name"] == "analysis" assert seen["body"]["expires_after"] == {"anchor": "last_active_at", "minutes": 30} def test_delete_sends_openai_beta_header_and_accepts_confirmation(monkeypatch): seen: dict = {} def handler(request: httpx.Request) -> httpx.Response: seen["headers"] = dict(request.headers) seen["url"] = str(request.url) seen["method"] = request.method return httpx.Response( 200, json = {"id": "cntr_x", "object": "container.deleted", "deleted": True}, ) _mock_http_client(monkeypatch, handler) _drive(_make_client().delete_openai_container("cntr_x")) assert seen["method"] == "DELETE" assert seen["url"] == "https://api.openai.com/v1/containers/cntr_x" assert seen["headers"].get("openai-beta") == "containers=v1" def test_delete_raises_when_response_lacks_deleted_true(monkeypatch): """OpenAI returns 200 ``{"deleted": true}`` even when the request is silently rejected (e.g. before we sent OpenAI-Beta). Guard: when the body omits ``deleted: true``, surface an error so the UI reports the failure instead of false success.""" def handler(request: httpx.Request) -> httpx.Response: # 200 but no deleted flag — unexpected payload shape. return httpx.Response(200, json = {"id": "cntr_x", "object": "container"}) _mock_http_client(monkeypatch, handler) with pytest.raises(httpx.HTTPError, match = "did not confirm container deletion"): _drive(_make_client().delete_openai_container("cntr_x")) def test_delete_raises_when_deleted_is_false(monkeypatch): def handler(request: httpx.Request) -> httpx.Response: return httpx.Response( 200, json = {"id": "cntr_x", "object": "container.deleted", "deleted": False}, ) _mock_http_client(monkeypatch, handler) with pytest.raises(httpx.HTTPError, match = "did not confirm container deletion"): _drive(_make_client().delete_openai_container("cntr_x")) def test_delete_raises_when_body_is_not_json(monkeypatch): def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, content = b"OK") _mock_http_client(monkeypatch, handler) with pytest.raises(httpx.HTTPError, match = "did not confirm container deletion"): _drive(_make_client().delete_openai_container("cntr_x")) def test_delete_propagates_openai_4xx(monkeypatch): def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(404, json = {"error": {"message": "not found"}}) _mock_http_client(monkeypatch, handler) with pytest.raises(httpx.HTTPStatusError): _drive(_make_client().delete_openai_container("cntr_missing")) def test_list_route_filters_expired_containers(monkeypatch): """OpenAI keeps containers in /v1/containers with status="expired" after their idle TTL passes — unusable but still listed. The list route must drop them so the picker shows only usable containers.""" from routes import inference as inf_mod from models.inference import OpenAIContainerRequest def handler(request: httpx.Request) -> httpx.Response: return httpx.Response( 200, json = { "data": [ {"id": "cntr_active", "name": "live", "status": "running"}, {"id": "cntr_dead", "name": "old", "status": "expired"}, {"id": "cntr_unknown", "name": "no-status"}, ], }, ) _mock_http_client(monkeypatch, handler) def fake_resolve(_body): return _make_client() monkeypatch.setattr(inf_mod, "_resolve_openai_cloud_client", fake_resolve) body = OpenAIContainerRequest( encrypted_api_key = "enc", provider_base_url = "https://api.openai.com/v1", ) response = _drive(inf_mod.list_openai_containers(body, current_subject = "u")) ids = [c.id for c in response.containers] assert "cntr_active" in ids assert "cntr_unknown" in ids # missing status is treated as usable assert "cntr_dead" not in ids