""" Tests for TatlockOllamaProvider configuration and wiring. Every assertion here goes through `provider.client` — the property pydantic_ai actually reads. The previous version asserted on `_openai_client`, a lookalike attribute nobody read, and stayed green for the whole period the sanitizer was bypassed in production (T-6): a test whose subject is not the real thing cannot fail for the real reason. """ import pytest from src.core.config import config from src.ollama import provider as provider_module from src.ollama.provider import ( TatlockOllamaProvider, _parent_chat, _sanitize_messages, _SanitizedAsyncOpenAI, _SanitizedCompletions, ) @pytest.mark.unit class TestClientWiring: """The sanitized client must be the one the provider serves (T-6).""" def test_provider_serves_the_sanitized_client(self): provider = TatlockOllamaProvider(base_url="http://localhost:11434/v1") assert isinstance(provider.client, _SanitizedAsyncOpenAI) def test_the_full_chain_reaches_completions(self): # Walks provider.client -> sanitized chat -> parent lookup -> # completions. Under openai's cached_property `chat`, the old # hardcoded `.fget` access raised AttributeError right here. provider = TatlockOllamaProvider(base_url="http://localhost:11434/v1") assert provider.client.chat.completions is not None def test_parent_lookup_survives_either_descriptor_shape(self): from openai import AsyncOpenAI descriptor = AsyncOpenAI.__dict__["chat"] assert hasattr(descriptor, "func") or hasattr( descriptor, "fget" ), "AsyncOpenAI.chat exposes neither getter; _parent_chat needs updating" client = _SanitizedAsyncOpenAI(base_url="http://localhost:11434/v1") parent = _parent_chat(client) assert parent is not None assert ( type(parent).__name__ != "_SanitizedChat" ), "parent lookup must not recurse into the override" @pytest.mark.unit class TestProviderTimeout: """Timeout configuration on the client the provider actually serves.""" def test_openai_client_timeout_from_config(self): provider = TatlockOllamaProvider(base_url="http://localhost:11434/v1") assert provider.client.timeout == float(config.OLLAMA_TIMEOUT) def test_timeout_is_not_sdk_default(self): provider = TatlockOllamaProvider(base_url="http://localhost:11434/v1") # The OpenAI SDK defaults to 600s; the configured cap must win assert provider.client.timeout < 600 @pytest.mark.unit class TestMessageSanitization: """Null content sanitization for Ollama compatibility.""" def test_null_content_with_tool_calls_becomes_empty_string(self): messages = [ { "role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "type": "function"}], } ] sanitized = _sanitize_messages(messages) assert sanitized[0]["content"] == "" def test_regular_messages_unchanged(self): messages = [ {"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Good day, sir."}, ] assert _sanitize_messages(messages) == messages class _StubLogger: def __init__(self): self.calls = [] def info(self, event, **kwargs): self.calls.append(("info", event, kwargs)) def warning(self, event, **kwargs): self.calls.append(("warning", event, kwargs)) class _BusyResponse: def __init__(self): self.model_extra = { "balancing": [{"parked": "librarian"}], "compaction_due": True, } class _QuietResponse: def __init__(self): self.model_extra = {"balancing": [], "compaction_due": False} @pytest.mark.unit class TestWrapperSignals: """The wrapper's body signals must not pass silently (T-4/T-6). Read from the parsed body's model_extra: an httpx event-hook variant demonstrably never fires under the openai SDK. """ def test_busy_response_logs_balancing_and_compaction(self, monkeypatch): stub = _StubLogger() monkeypatch.setattr(provider_module, "logger", stub) provider_module._log_wrapper_signals(_BusyResponse()) assert any(kind == "info" and event == "backend_balancing" for kind, event, _ in stub.calls) assert any(kind == "warning" for kind, _, _ in stub.calls) def test_quiet_response_logs_nothing(self, monkeypatch): stub = _StubLogger() monkeypatch.setattr(provider_module, "logger", stub) provider_module._log_wrapper_signals(_QuietResponse()) assert stub.calls == [] async def test_create_surfaces_the_signals(self, monkeypatch): # The read must sit in the request path, not merely exist: # removing the call from create() has to fail this test. stub = _StubLogger() monkeypatch.setattr(provider_module, "logger", stub) class FakeOriginal: async def create(self, **kwargs): return _BusyResponse() completions = _SanitizedCompletions(FakeOriginal()) await completions.create(messages=[{"role": "user", "content": "hi"}]) assert any(event == "backend_balancing" for _, event, _ in stub.calls)