Electronics Catalog: API, MCP server, frontend and deployment
Verified catalogue of mobiles and laptops sold in India, collected from real retail listings (FastAPI backend, React frontend, Postgres/pgvector). - REST API under /api/elec (read-only catalogue; admin endpoints need login) - MCP server (FastMCP) at /mcp/ with list_categories, search_products, get_product and price_history tools - Real ratings and reviews read from product pages and search results - Production Dockerfile (requirements-api.txt, no PyTorch) and .env.production.example; remote database only via an explicit ELEC_ALLOW_REMOTE_DB host/name allowlist - docs/API.md: endpoint and MCP reference with live examples Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
backend/tests/test_ollama_reachability.py
Normal file
108
backend/tests/test_ollama_reachability.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user