ChokePointAI / tests /integration /test_simulate_failover.py
YuraBodnar's picture
test: add extraction quality evals and failover integration tests
f278c3f
Raw History Blame Contribute Delete
4.21 kB
"""POST /api/v1/simulate while Hugging Face is down: 424 first, local only on ``force_local``."""
from __future__ import annotations
from collections.abc import Iterator
import httpx
import pytest
from fastapi.testclient import TestClient
from huggingface_hub.errors import HfHubHTTPError
from chokepoint.agent.extractor import ExtractionAgent
from chokepoint.agent.providers.base import Message
from chokepoint.agent.providers.failover import FailoverProvider
from chokepoint.agent.providers.stub import DEFAULT_RESPONSE
from chokepoint.agent.resolver import load_aliases
from chokepoint.api import deps
from chokepoint.api.main import app
from chokepoint.contracts import SimulateRequest
TEXT = {"text": "Dockworkers strike at Hamburg Port halts container handling."}
class DownHF:
name = "hf"
supports_structured = True
def __init__(self) -> None:
self.calls = 0
def chat(self, messages: list[Message], *, structured: bool = True) -> str:
self.calls += 1
resp = httpx.Response(
402, request=httpx.Request("POST", "https://router.huggingface.co/v1/chat/completions")
)
raise HfHubHTTPError("402 Client Error: credits depleted", response=resp)
class LocalModel:
name = "local"
supports_structured = False
def __init__(self) -> None:
self.calls = 0
def chat(self, messages: list[Message], *, structured: bool = True) -> str:
self.calls += 1
return DEFAULT_RESPONSE
@pytest.fixture
def hf_down(monkeypatch: pytest.MonkeyPatch) -> Iterator[tuple[DownHF, LocalModel]]:
hf, local = DownHF(), LocalModel()
agent = ExtractionAgent(FailoverProvider(hf, local, require_consent=True), load_aliases())
monkeypatch.setenv("DEMO_MODE", "false")
monkeypatch.setenv("GRAPH_BACKEND", "networkx")
deps.get_settings.cache_clear()
deps.get_graph_store.cache_clear()
app.dependency_overrides[deps.get_llm_provider] = lambda: agent
yield hf, local
app.dependency_overrides.pop(deps.get_llm_provider, None)
deps.get_settings.cache_clear()
deps.get_graph_store.cache_clear()
def test_hf_outage_returns_424_with_an_actionable_payload(
client: TestClient, hf_down: tuple[DownHF, LocalModel]
) -> None:
hf, local = hf_down
resp = client.post("/api/v1/simulate", json=TEXT)
assert resp.status_code == 424
detail = resp.json()["detail"]
assert detail["code"] == "hf_unavailable"
assert "Hugging Face" in detail["message"]
assert "HfHubHTTPError" in detail["reason"]
assert (hf.calls, local.calls) == (1, 0) # local never starts without consent
def test_force_local_runs_the_local_model_and_skips_hf(
client: TestClient, hf_down: tuple[DownHF, LocalModel]
) -> None:
hf, local = hf_down
resp = client.post("/api/v1/simulate", json={**TEXT, "force_local": True})
assert resp.status_code == 200
body = resp.json()
assert body["event"]["extractor_version"].startswith("local-")
assert len(body["impacted"]) > 0
assert (hf.calls, local.calls) == (0, 1)
def test_local_consent_does_not_stick_to_later_requests(
client: TestClient, hf_down: tuple[DownHF, LocalModel]
) -> None:
hf, local = hf_down
assert client.post("/api/v1/simulate", json={**TEXT, "force_local": True}).status_code == 200
again = client.post("/api/v1/simulate", json=TEXT)
assert again.status_code == 424 # the user is asked again; HF was retried, not skipped
assert (hf.calls, local.calls) == (1, 1)
def test_force_local_is_harmless_when_an_event_is_supplied(
client: TestClient, hf_down: tuple[DownHF, LocalModel]
) -> None:
hf, local = hf_down
payload = {
"event": {
"event_type": "strike",
"locations": [{"raw": "Hamburg Port", "node_id": "port_hamburg"}],
"severity": 3,
"confidence": 0.8,
"summary": "test",
},
"force_local": True,
}
assert client.post("/api/v1/simulate", json=payload).status_code == 200
assert (hf.calls, local.calls) == (0, 0) # no LLM involved at all
def test_force_local_defaults_to_false() -> None:
assert SimulateRequest(text="x").force_local is False