From 35d5f4416eaf26dbcf3ddb742cb8fd052a8cff9c Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 5 May 2026 09:39:20 +0300 Subject: [PATCH] fill in vLLM error-path and MCP image-query coverage --- tests/test_embedder.py | 205 +++++++++++++++++++++++++++++++++++++++++ tests/test_mcp.py | 27 ++++++ 2 files changed, 232 insertions(+) diff --git a/tests/test_embedder.py b/tests/test_embedder.py index a583deb8..2a5d62c0 100644 --- a/tests/test_embedder.py +++ b/tests/test_embedder.py @@ -362,6 +362,211 @@ async def test_vllm_supports_images_flag(): assert embedder.supports_images is True +async def test_vllm_connect_error_surfaces_helpful_message(monkeypatch): + import httpx + + from haiku.rag.embeddings.vllm import VLLMMultimodalEmbedder + + class FakeAsyncClient: + def __init__(self, *args, **kwargs): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + async def post(self, *args, **kwargs): + raise httpx.ConnectError("All connection attempts failed") + + monkeypatch.setattr("httpx.AsyncClient", FakeAsyncClient) + + embedder = VLLMMultimodalEmbedder( + model_name="x", vector_dim=2, base_url="http://nope:8000/v1" + ) + with pytest.raises(ValueError, match="Could not connect to vLLM"): + await embedder.embed_query("hi") + + +async def test_vllm_timeout_surfaces_helpful_message(monkeypatch): + import httpx + + from haiku.rag.embeddings.vllm import VLLMMultimodalEmbedder + + class FakeAsyncClient: + def __init__(self, *args, **kwargs): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + async def post(self, *args, **kwargs): + raise httpx.TimeoutException("timed out") + + monkeypatch.setattr("httpx.AsyncClient", FakeAsyncClient) + + embedder = VLLMMultimodalEmbedder( + model_name="x", vector_dim=2, base_url="http://localhost:8000/v1" + ) + with pytest.raises(ValueError, match="timed out"): + await embedder.embed_query("hi") + + +async def test_vllm_401_surfaces_auth_error(monkeypatch): + import httpx + + from haiku.rag.embeddings.vllm import VLLMMultimodalEmbedder + + class FakeResponse: + status_code = 401 + + def raise_for_status(self): + raise httpx.HTTPStatusError( + "401", + request=httpx.Request("POST", "http://x"), + response=self, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + ) + + class FakeAsyncClient: + def __init__(self, *args, **kwargs): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + async def post(self, *args, **kwargs): + return FakeResponse() + + monkeypatch.setattr("httpx.AsyncClient", FakeAsyncClient) + + embedder = VLLMMultimodalEmbedder( + model_name="x", vector_dim=2, base_url="http://localhost:8000/v1", api_key="bad" + ) + with pytest.raises(ValueError, match="Authentication failed"): + await embedder.embed_query("hi") + + +async def test_vllm_other_http_error_surfaces(monkeypatch): + import httpx + + from haiku.rag.embeddings.vllm import VLLMMultimodalEmbedder + + class FakeResponse: + status_code = 500 + + def raise_for_status(self): + raise httpx.HTTPStatusError( + "500", + request=httpx.Request("POST", "http://x"), + response=self, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + ) + + class FakeAsyncClient: + def __init__(self, *args, **kwargs): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + async def post(self, *args, **kwargs): + return FakeResponse() + + monkeypatch.setattr("httpx.AsyncClient", FakeAsyncClient) + + embedder = VLLMMultimodalEmbedder( + model_name="x", vector_dim=2, base_url="http://localhost:8000/v1" + ) + with pytest.raises(ValueError, match="HTTP error from vLLM"): + await embedder.embed_query("hi") + + +async def test_vllm_empty_data_response_raises(monkeypatch): + from haiku.rag.embeddings.vllm import VLLMMultimodalEmbedder + + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return {"data": []} + + class FakeAsyncClient: + def __init__(self, *args, **kwargs): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + async def post(self, *args, **kwargs): + return FakeResponse() + + monkeypatch.setattr("httpx.AsyncClient", FakeAsyncClient) + + embedder = VLLMMultimodalEmbedder( + model_name="x", vector_dim=2, base_url="http://localhost:8000/v1" + ) + with pytest.raises(ValueError, match="returned no embeddings"): + await embedder.embed_query("hi") + + +async def test_vllm_embed_documents_empty_list_skips_request(monkeypatch): + from haiku.rag.embeddings.vllm import VLLMMultimodalEmbedder + + called = False + + class FakeAsyncClient: + def __init__(self, *args, **kwargs): + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + async def post(self, *args, **kwargs): + nonlocal called + called = True + + monkeypatch.setattr("httpx.AsyncClient", FakeAsyncClient) + + embedder = VLLMMultimodalEmbedder( + model_name="x", vector_dim=2, base_url="http://localhost:8000/v1" + ) + assert await embedder.embed_documents([]) == [] + assert called is False + + +async def test_vllm_pil_image_roundtrips_to_data_uri(): + from PIL import Image + + from haiku.rag.embeddings.vllm import _to_data_uri + + img = Image.new("RGB", (4, 4), color="red") + uri = _to_data_uri(img) + assert uri.startswith("data:image/png;base64,") + + +async def test_vllm_to_data_uri_rejects_unsupported(): + from haiku.rag.embeddings.vllm import _to_data_uri + + with pytest.raises(TypeError): + _to_data_uri("not bytes") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + + async def test_vllm_get_embedder_routes_to_multimodal(): config = AppConfig( embeddings=EmbeddingsConfig( diff --git a/tests/test_mcp.py b/tests/test_mcp.py index c9220005..a88dd2b3 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -240,3 +240,30 @@ class TestMCPImageQuery: results = await search_by_image(image_base64=png_b64) # Empty list is fine (the stub vector won't match the toy fixture). assert isinstance(results, list) + + @pytest.mark.asyncio + async def test_image_query_returns_empty_on_invalid_base64( + self, mcp_db, monkeypatch + ): + """Garbage base64 from the caller is swallowed, returning an empty + list rather than crashing the MCP server.""" + from haiku.rag.embeddings import EmbedderWrapper + + class StubMultimodal(EmbedderWrapper): + supports_images = True + + def __init__(self): + super().__init__(embedder=None, vector_dim=2560) + + monkeypatch.setattr( + "haiku.rag.embeddings.get_embedder", + lambda *a, **kw: StubMultimodal(), + ) + + mcp = create_mcp_server(mcp_db, read_only=True) + search_by_image = await _get_tool(mcp, "search_documents_by_image") + + # Not valid base64 (contains non-base64 chars) — the strict decoder + # in search_documents_by_image rejects it. + results = await search_by_image(image_base64="!!! not base64 !!!") + assert results == []