"""Contract tests for the Jev-compatible API (DESIGN ยง4). Needs an export dir: JEV_EXPORT_DIR=exports/... pytest -m gpu tests/test_server_contract.py""" import os import pytest pytestmark = pytest.mark.gpu EXPORT = os.environ.get("JEV_EXPORT_DIR", "") @pytest.fixture(scope="module") def client(): if not EXPORT or not os.path.isdir(EXPORT): pytest.skip("set JEV_EXPORT_DIR to an export bundle") from fastapi.testclient import TestClient from jev_judge.server import Engine, create_app return TestClient(create_app(Engine(EXPORT))) def test_health_and_version_header(client): r = client.get("/healthz") assert r.status_code == 200 and r.json()["ok"] and r.json()["calibrated"] assert "X-Jev-Judge-Version" in r.headers def test_noul(client): r = client.post("/v1/decisions", json={"kind": "noul", "state": "Payment failed twice with card declined.", "question": "Is the customer asking for a refund?", "options": ["false", "true"]}) assert r.status_code == 200, r.text j = r.json() assert j["kind"] == "noul" and j["options"] == ["false", "true"] assert abs(sum(j["distribution"]) - 1) < 1e-4 and len(j["distribution"]) == 2 assert isinstance(j["decision"]["noul"], bool) and j["decision"]["choice"] is None assert j["model"]["calibrated"] is True and j["latency_ms"] > 0 def test_choice_alignment_and_sum(client): opts = ["approve", "approve_reduced", "deny", "request_more_info"] r = client.post("/v1/decisions", json={"kind": "choice", "state": "Applicant income 30k, debt 45k, two missed payments.", "question": "Loan decision?", "options": opts}) assert r.status_code == 200, r.text j = r.json() assert j["options"] == opts and len(j["distribution"]) == 4 assert abs(sum(j["distribution"]) - 1) < 1e-4 assert j["decision"]["choice"] == opts[max(range(4), key=lambda i: j["distribution"][i])] def test_score(client): r = client.post("/v1/decisions", json={"kind": "score", "state": "Server room temperature 41C, alarms ignored for 2 hours.", "question": "Rate urgency 0-5.", "options": ["0", "1", "2", "3", "4", "5"]}) assert r.status_code == 200, r.text j = r.json() assert len(j["distribution"]) == 6 and 0 <= j["decision"]["expected_score"] <= 5 assert j["decision"]["score"] in range(6) def test_422_bad_kind_and_options(client): assert client.post("/v1/decisions", json={"kind": "rank", "state": "x", "question": "y", "options": ["a", "b"]}).status_code == 422 assert client.post("/v1/decisions", json={"kind": "noul", "state": "x", "question": "y", "options": ["yes", "no"]}).status_code == 422 assert client.post("/v1/decisions", json={"kind": "choice", "state": "x", "question": "y", "options": ["only"]}).status_code == 422 assert client.post("/v1/decisions", json={"kind": "choice", "state": "x", "question": "y", "options": [str(i) for i in range(17)]}).status_code == 422 def test_413_too_long_without_truncate(client): long_state = "word " * 3000 r = client.post("/v1/decisions", json={"kind": "noul", "state": long_state, "question": "q?", "options": ["false", "true"], "truncate": False}) assert r.status_code == 413 r = client.post("/v1/decisions", json={"kind": "noul", "state": long_state, "question": "q?", "options": ["false", "true"], "truncate": True}) assert r.status_code == 200 def test_batch_order_and_limit(client): items = [{"kind": "noul", "state": f"case {i}", "question": "ok?", "options": ["false", "true"]} for i in range(5)] items.insert(2, {"kind": "choice", "state": "s", "question": "q", "options": ["a", "b", "c"]}) r = client.post("/v1/decisions:batch", json={"items": items}) assert r.status_code == 200, r.text res = r.json()["results"] assert len(res) == 6 and res[2]["kind"] == "choice" and res[0]["kind"] == "noul" too_many = [items[0]] * 257 assert client.post("/v1/decisions:batch", json={"items": too_many}).status_code == 413