"""Phase-2 tests for the Jev client — MOCKED HTTP (no network, no key needed). docker compose exec bot python tools/test_jev.py """ import asyncio import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from app import jev from app.config import settings PASSED: list[str] = [] FAILED: list[str] = [] def check(name: str, ok: bool, extra: str = "") -> None: (PASSED if ok else FAILED).append(name) print(f"{'PASS' if ok else 'FAIL'} {name}{' — ' + str(extra) if extra else ''}") class FakeResp: def __init__(self, status: int, body: dict): self.status_code = status self._body = body self.text = str(body) def json(self): return self._body class FakeClient: """Scripted httpx.AsyncClient stand-in: pops one (status, body) per POST.""" script: list = [] posts: int = 0 def __init__(self, *a, **k): pass async def __aenter__(self): return self async def __aexit__(self, *a): return False async def post(self, url, json=None, headers=None): FakeClient.posts += 1 status, body = FakeClient.script.pop(0) return FakeResp(status, body) Q = {"m0": {"type": "choice", "instructions": "i", "criteria": {"x": ""}}} async def main() -> None: orig_client = jev.httpx.AsyncClient saved_key = settings.TYPESAFE_API_KEY jev.httpx.AsyncClient = FakeClient try: # 1) no key -> None (no HTTP at all) settings.TYPESAFE_API_KEY = "" FakeClient.posts = 0 r = await jev.ask({"s": 1}, Q) check("no api key -> None (no request)", r is None and FakeClient.posts == 0) settings.TYPESAFE_API_KEY = saved_key or "test-key" # 2) happy path: answers + model + usage + latency FakeClient.script = [(200, {"model": "jev-1.13.0", "answers": {"m0": {"choice": "x", "confidence": 0.9, "probabilities": {"x": 0.9}}}, "usage": {"input_tokens": 10, "output_tokens": 2}})] r = await jev.ask({"s": 1}, Q) ok = bool(r) and r["answers"]["m0"]["choice"] == "x" and r["model"] == "jev-1.13.0" \ and r["usage"]["input_tokens"] == 10 and isinstance(r["latency_ms"], int) check("happy path -> answers/model/usage/latency", ok) # 3) 429 -> one retry, then success FakeClient.script = [(429, {"error": "rate_limited"}), (200, {"model": "jev-1.13.0", "answers": {}, "usage": {}})] FakeClient.posts = 0 r = await jev.ask({"s": 1}, Q) check("429 retried once then succeeds", r is not None and FakeClient.posts == 2) # 4) hard 400 -> None, no retry FakeClient.script = [(400, {"error": "bad_request"})] FakeClient.posts = 0 r = await jev.ask({"s": 1}, Q) check("400 -> None without retry", r is None and FakeClient.posts == 1) # 5) exception -> retried once -> None class BoomClient(FakeClient): async def post(self, url, json=None, headers=None): FakeClient.posts += 1 raise RuntimeError("net down") jev.httpx.AsyncClient = BoomClient FakeClient.posts = 0 r = await jev.ask({"s": 1}, Q) check("exception retried once -> None", r is None and FakeClient.posts == 2) finally: jev.httpx.AsyncClient = orig_client settings.TYPESAFE_API_KEY = saved_key total = len(PASSED) + len(FAILED) print(f"\n{len(PASSED)}/{total} passed") if FAILED: print("FAILED:", FAILED) raise SystemExit(1) if __name__ == "__main__": asyncio.run(main())