"""The reachability probe in front of every Ollama call. WHY THIS FILE EXISTS -------------------- `_ensure_client()` asks Ollama for `/api/tags` with a 5-second timeout, and `stage_2_row_intake` calls it once per ROW through `fetch_product_details`. Uncached, a 2000-row sheet ingested with `use_llm` on, against a configured but unreachable Ollama, spends up to ~2.8 hours doing nothing but timing out - and shows as a batch that has hung, not one that has failed. That was survivable only while `use_llm` defaulted to false everywhere. It no longer does: `UPLOAD_AUTORUN_USE_LLM` is true, so every auto-started upload now takes this path. The cache is what makes that default safe, and the first test below is the one that stops it being quietly removed in a later refactor. `/api/health` calls the same function, so a down Ollama also stops adding five seconds to every health request. """ from __future__ import annotations import pytest from app.services import ollama_service @pytest.fixture(autouse=True) def _clean_probe_cache(): """The cache is a module global and outlives a test.""" ollama_service.reset_reachability_cache() yield ollama_service.reset_reachability_cache() @pytest.fixture def probe_calls(monkeypatch): """Count the HTTP probes, and make every one of them fail. Failure is the case that matters: a reachable Ollama answers in milliseconds, an unreachable one costs the full timeout, and it is the second that used to be paid per row. """ calls: list = [] def boom(url, **kwargs): calls.append(url) raise OSError("connection refused") monkeypatch.setattr(ollama_service, "USE_OLLAMA", True) monkeypatch.setattr(ollama_service.requests, "get", boom) return calls def test_an_unreachable_ollama_is_probed_once_not_once_per_call(probe_calls): """The whole point. Ten rows must not be ten timeouts.""" for _ in range(10): assert ollama_service._ensure_client() is False assert len(probe_calls) == 1, ( f"{len(probe_calls)} probes for 10 calls - the cache is not holding, and " f"an ingest will pay the 5s timeout per row" ) def test_the_cache_expires_so_a_late_start_is_noticed(probe_calls, monkeypatch): """A permanent memo would mean an Ollama started after the API is never seen, and /api/health reports it down until someone redeploys.""" clock = [1000.0] monkeypatch.setattr(ollama_service.time, "monotonic", lambda: clock[0]) ollama_service._ensure_client() assert len(probe_calls) == 1 clock[0] += ollama_service._PROBE_TTL_SECONDS + 1 ollama_service._ensure_client() assert len(probe_calls) == 2, "the probe never expired" def test_a_reachable_ollama_is_also_cached(monkeypatch): """Both outcomes are cached. Caching only the failure would leave the happy path paying an HTTP round trip per row - cheap, but per row and pointless.""" calls: list = [] class Ok: status_code = 200 def ok(url, **kwargs): calls.append(url) return Ok() monkeypatch.setattr(ollama_service, "USE_OLLAMA", True) monkeypatch.setattr(ollama_service.requests, "get", ok) assert [ollama_service._ensure_client() for _ in range(5)] == [True] * 5 assert len(calls) == 1 def test_disabled_stays_none_and_never_touches_the_network(monkeypatch): """Three return values, not two: `system.py` tells "switched off" from "configured but down", and /api/health's `ollama` field means different things in each case. Collapsing this to a bool would break that. """ def never(*_args, **_kwargs): raise AssertionError("USE_OLLAMA is false - nothing may be requested") monkeypatch.setattr(ollama_service, "USE_OLLAMA", False) monkeypatch.setattr(ollama_service.requests, "get", never) assert ollama_service._ensure_client() is None