11"""Unit tests for the /health REST API endpoint."""
22
3+ from typing import Any
4+
35import pytest
46from llama_stack_client import APIConnectionError
57from pytest_mock import MockerFixture
68
79from app .endpoints .health import (
810 HealthStatus ,
11+ check_default_model_available ,
912 get_providers_health_statuses ,
1013 liveness_probe_get_method ,
1114 readiness_probe_get_method ,
@@ -72,6 +75,12 @@ async def test_readiness_probe_success_when_all_providers_healthy(
7275 ),
7376 ]
7477
78+ # Mock check_default_model_available so it doesn't hit uninitialized client
79+ mock_check_model = mocker .patch (
80+ "app.endpoints.health.check_default_model_available"
81+ )
82+ mock_check_model .return_value = (True , "Default model is available" )
83+
7584 # Mock the Response object and auth
7685 mock_response = mocker .Mock ()
7786
@@ -87,6 +96,43 @@ async def test_readiness_probe_success_when_all_providers_healthy(
8796 assert len (response .providers ) == 0
8897
8998
99+ @pytest .mark .asyncio
100+ async def test_readiness_probe_fails_when_model_not_available (
101+ mocker : MockerFixture ,
102+ ) -> None :
103+ """Test readiness returns 503 when providers are healthy but default model is missing."""
104+ mock_authorization_resolvers (mocker )
105+
106+ mock_get_providers = mocker .patch (
107+ "app.endpoints.health.get_providers_health_statuses"
108+ )
109+ mock_get_providers .return_value = [
110+ ProviderHealthStatus (
111+ provider_id = "provider1" ,
112+ status = HealthStatus .OK .value ,
113+ message = "Provider is healthy" ,
114+ )
115+ ]
116+
117+ mock_check_model = mocker .patch (
118+ "app.endpoints.health.check_default_model_available"
119+ )
120+ mock_check_model .return_value = (
121+ False ,
122+ "Default model google-vertex/publishers/google/models/gemini-2.5-flash "
123+ "not found in model registry" ,
124+ )
125+
126+ mock_response = mocker .Mock ()
127+ auth : AuthTuple = ("test_user_id" , "test_user" , True , "test_token" )
128+
129+ response = await readiness_probe_get_method (auth = auth , response = mock_response )
130+
131+ assert response .ready is False
132+ assert "not found in model registry" in response .reason
133+ assert mock_response .status_code == 503
134+
135+
90136@pytest .mark .asyncio
91137async def test_liveness_probe (mocker : MockerFixture ) -> None :
92138 """Test the liveness endpoint handler."""
@@ -207,3 +253,87 @@ async def test_get_providers_health_statuses_connection_error(
207253 assert (
208254 result [0 ].message == "Failed to initialize health check: Connection error."
209255 )
256+
257+
258+ class TestCheckDefaultModelAvailable :
259+ """Test cases for the check_default_model_available function.
260+
261+ The model availability logic (registry lookup, reload, error handling)
262+ is tested in tests/unit/test_client.py (TestCheckModelAvailable). These
263+ tests verify only the config lookup and delegation in health.py.
264+ """
265+
266+ EXPECTED_MODEL_ID = "google-vertex/publishers/google/models/gemini-2.5-flash"
267+
268+ @pytest .fixture
269+ def inference_config (self , mocker : MockerFixture ) -> Any :
270+ """Patch configuration with default model and provider."""
271+ mock_config = mocker .patch ("app.endpoints.health.configuration" )
272+ mock_config .inference .default_model = (
273+ "publishers/google/models/gemini-2.5-flash"
274+ )
275+ mock_config .inference .default_provider = "google-vertex"
276+ return mock_config
277+
278+ @pytest .mark .asyncio
279+ async def test_no_inference_config (self , mocker : MockerFixture ) -> None :
280+ """Test returns True when no inference configuration exists."""
281+ mock_config = mocker .patch ("app.endpoints.health.configuration" )
282+ mock_config .inference = None
283+
284+ available , reason = await check_default_model_available ()
285+
286+ assert available is True
287+ assert reason == "No default model configured"
288+
289+ @pytest .mark .asyncio
290+ async def test_no_default_model_configured (self , mocker : MockerFixture ) -> None :
291+ """Test returns True when no default model is configured."""
292+ mock_config = mocker .patch ("app.endpoints.health.configuration" )
293+ mock_config .inference .default_model = None
294+ mock_config .inference .default_provider = None
295+
296+ available , reason = await check_default_model_available ()
297+
298+ assert available is True
299+ assert reason == "No default model configured"
300+
301+ @pytest .mark .asyncio
302+ @pytest .mark .usefixtures ("inference_config" )
303+ async def test_delegates_to_client_holder (
304+ self ,
305+ mocker : MockerFixture ,
306+ ) -> None :
307+ """Test delegates to client holder with correct model ID."""
308+ mock_holder = mocker .patch ("app.endpoints.health.AsyncLlamaStackClientHolder" )
309+ mock_holder .return_value .check_model_available = mocker .AsyncMock (
310+ return_value = (True , f"Model { self .EXPECTED_MODEL_ID } is available" )
311+ )
312+
313+ available , reason = await check_default_model_available ()
314+
315+ assert available is True
316+ assert "is available" in reason
317+ mock_holder .return_value .check_model_available .assert_awaited_once_with (
318+ self .EXPECTED_MODEL_ID
319+ )
320+
321+ @pytest .mark .asyncio
322+ @pytest .mark .usefixtures ("inference_config" )
323+ async def test_returns_holder_failure (
324+ self ,
325+ mocker : MockerFixture ,
326+ ) -> None :
327+ """Test passes through failure result from client holder."""
328+ mock_holder = mocker .patch ("app.endpoints.health.AsyncLlamaStackClientHolder" )
329+ mock_holder .return_value .check_model_available = mocker .AsyncMock (
330+ return_value = (
331+ False ,
332+ f"Model { self .EXPECTED_MODEL_ID } not found in model registry" ,
333+ )
334+ )
335+
336+ available , reason = await check_default_model_available ()
337+
338+ assert available is False
339+ assert "not found in model registry" in reason
0 commit comments