import pytest from fastapi import FastAPI from httpx import AsyncClient from src.bootstrap.lifespan import _warm_dense_embedders from src.config import Settings from tests.fakes import FakeDenseEmbedder pytestmark = [pytest.mark.unit, pytest.mark.asyncio] async def test_lifespan_starts_and_stops_without_docker(client: AsyncClient) -> None: response = await client.get("/healthz") assert response.status_code == 200 async def test_lifespan_binds_resources_to_app_state(app: FastAPI, client: AsyncClient) -> None: await client.get("/healthz") resources = app.state.resources assert isinstance(resources.settings, Settings) assert resources.minio_client is not None assert len(resources.dense_embedders) == 2 assert {e.name for e in resources.dense_embedders} == {"dense_nomic", "dense_openai"} assert resources.sparse_embedder.name == "sparse" assert resources.ingestion_concurrency_limiter is not None async def test_warm_dense_embedders_calls_every_embedder() -> None: """Pays the model-load cost at boot instead of on a user's first upload: a cold nomic load outruns INGESTION_TIMEOUT_SECONDS entirely. """ embedders = [FakeDenseEmbedder(name="dense_nomic"), FakeDenseEmbedder(name="dense_openai")] await _warm_dense_embedders(embedders) assert all(e.calls for e in embedders) async def test_warm_dense_embedders_survives_an_unreachable_embedder() -> None: """A down embedder must not stop the process booting -- otherwise the service cannot come up to report its own health. `/readyz` owns that signal, not startup. """ failing = FakeDenseEmbedder(name="dense_nomic", fail_next=True) healthy = FakeDenseEmbedder(name="dense_openai") await _warm_dense_embedders([failing, healthy]) # The failure is swallowed *and* does not abort the remaining warm-ups. assert healthy.calls