"""Health endpoint contract tests (handbook 10-platform/health.md)."""
from unittest.mock import AsyncMock
import pytest
from fastapi.testclient import TestClient
import navi.api.routes.health as health_mod
from navi.config import Settings
@pytest.fixture
def health_client(monkeypatch):
new_settings = Settings(_env_file=None, navi_persona_file="", hive_url="")
monkeypatch.setattr(health_mod, "settings", new_settings)
from navi.main import app
return TestClient(app)
@pytest.fixture(autouse=True)
def _fresh_embed_cache():
health_mod._embed_cache["checked_at"] = 0.0
health_mod._embed_cache["result"] = None
yield
def test_health_ok_when_all_checks_pass(health_client, monkeypatch):
monkeypatch.setattr(
health_mod, "_check_embed",
AsyncMock(return_value={"ok": True, "backend": "ollama:11434", "error": None}),
)
resp = health_client.get("/health")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "ok"
assert body["service"] == "navi"
assert body["checks"]["embed"]["status"] == "ok"
assert body["checks"]["hive"]["status"] == "ok"
def test_health_degraded_when_embed_fails(health_client, monkeypatch):
monkeypatch.setattr(
health_mod, "_check_embed",
AsyncMock(return_value={"ok": False, "backend": "none", "error": "connection refused"}),
)
body = health_client.get("/health").json()
assert body["status"] == "degraded"
assert body["checks"]["embed"]["status"] == "degraded"
assert body["checks"]["embed"]["detail"] == "connection refused"
def test_health_keeps_legacy_embed_payload(health_client, monkeypatch):
"""The pre-contract top-level `embed` dict must survive for existing tooling."""
monkeypatch.setattr(
health_mod, "_check_embed",
AsyncMock(return_value={"ok": True, "backend": "ollama:11434", "error": None}),
)
body = health_client.get("/health").json()
assert body["embed"]["ok"] is True
assert body["embed"]["backend"] == "ollama:11434"
def test_embed_check_is_cached(health_client, monkeypatch):
check = AsyncMock(return_value={"ok": True, "backend": "x", "error": None})
monkeypatch.setattr(health_mod, "_check_embed", check)
first = health_client.get("/health").json()
second = health_client.get("/health").json()
assert check.await_count == 1 # second poll was served from cache
assert first["checks"]["embed"] == second["checks"]["embed"]
assert isinstance(first["checks"]["embed"]["ms"], int)
# ── llm route block ─────────────────────────────────────────────────────────
# "Which side of the pair is answering" is the point of the backup host, so
# /health says it — and degrades when the primary is not the one serving.
class _FakeLLMBackend:
def __init__(self, active, snapshot=None):
self._active = active
self.health = type("H", (), {"snapshot": lambda _self: snapshot or {}})()
def active_report(self):
return self._active
def _serve_llm(monkeypatch, backend):
import navi.api.deps as deps_mod
monkeypatch.setattr(
deps_mod, "_resolve_container", lambda: type("C", (), {"llm_backend": backend})()
)
def _primary_report():
return {
"default": {
"model": "glm-5.3-flash:cloud",
"target": "cloud",
"primary": True,
"primary_model": "glm-5.3-flash:cloud",
}
}
def _fallback_report():
return {
"default": {
"model": "qwen3.6:35b",
"target": "home",
"primary": False,
"primary_model": "glm-5.3-flash:cloud",
}
}
def test_health_llm_ok_when_the_primary_is_serving(health_client, monkeypatch):
monkeypatch.setattr(
health_mod, "_check_embed",
AsyncMock(return_value={"ok": True, "backend": "x", "error": None}),
)
_serve_llm(monkeypatch, _FakeLLMBackend(_primary_report()))
body = health_client.get("/health").json()
assert body["checks"]["llm"]["status"] == "ok"
assert body["llm"]["degraded"] is False
assert body["llm"]["active"]["default"]["target"] == "cloud"
assert body["status"] == "ok"
def test_health_degrades_when_a_route_runs_on_a_backup_entry(health_client, monkeypatch):
monkeypatch.setattr(
health_mod, "_check_embed",
AsyncMock(return_value={"ok": True, "backend": "x", "error": None}),
)
_serve_llm(monkeypatch, _FakeLLMBackend(_fallback_report()))
body = health_client.get("/health").json()
assert body["status"] == "degraded"
assert body["checks"]["llm"]["status"] == "degraded"
assert "qwen3.6:35b@home" in body["checks"]["llm"]["detail"]
def test_health_llm_exposes_target_states_and_missing_models(health_client, monkeypatch):
monkeypatch.setattr(
health_mod, "_check_embed",
AsyncMock(return_value={"ok": True, "backend": "x", "error": None}),
)
snapshot = {
"targets": {"cloud": {"state": "cooling", "reason": "quota", "retry_in_sec": 42}},
"missing_models": [{"target": "home", "model": "qwen3.6:35b", "detail": "404"}],
}
_serve_llm(monkeypatch, _FakeLLMBackend(_fallback_report(), snapshot))
llm = health_client.get("/health").json()["llm"]
assert llm["targets"]["cloud"]["reason"] == "quota"
assert llm["missing_models"][0]["model"] == "qwen3.6:35b"
def test_health_says_the_llm_is_not_configured_without_a_backend(health_client, monkeypatch):
monkeypatch.setattr(
health_mod, "_check_embed",
AsyncMock(return_value={"ok": True, "backend": "x", "error": None}),
)
_serve_llm(monkeypatch, None)
body = health_client.get("/health").json()
assert body["llm"] == {"configured": False}
assert body["checks"]["llm"] == {"status": "ok", "detail": "not configured"}