atlas-iac/testing/tests/test_hermes_batch_api.py

120 lines
4.4 KiB
Python

"""Verify the batch boundary fails closed before private source can be routed."""
import json
from urllib.error import URLError
import pytest
from services.hermes.scripts import batch_api as api
@pytest.fixture
def payload():
return {"model": "qwen3.5:9b", "prompt": "Return a JSON object containing ok=true.",
"stream": False, "think": False, "format": "json",
"options": {"num_ctx": 16384, "num_predict": 1024, "temperature": 0,
"top_p": 1, "top_k": 20, "seed": 42}}
@pytest.fixture
def upstream(monkeypatch):
calls = []
def respond(path, payload=None, timeout=10):
calls.append((path, payload))
if path == "/api/version":
return {"version": api.VERSION}
if path == "/api/tags":
return {"models": [{"name": n, "digest": d, "size": 123} for n, d in api.PINS.items()]}
return {"model": payload["model"], "done": True, "done_reason": "stop",
"message": {"content": '{"ok":true}', "thinking": "private internal reasoning"}, "context": [1]}
monkeypatch.setattr(api, "_request", respond)
return calls
def test_pinned_result_records_actual_settings_without_raw_thinking(payload, upstream):
status, result = api.generate(json.dumps(payload))
assert status == 200
assert "thinking" not in result and "context" not in result
assert result["batch_provenance"]["model_digest"] == api.PINS[payload["model"]]
assert result["batch_provenance"]["options"]["num_gpu"] == 0
assert upstream[-1][0] == "/api/chat"
assert upstream[-1][1]["messages"] == [{"role": "user", "content": payload["prompt"]}]
@pytest.mark.parametrize("change", [
{"model": "qwen3.6:27b-cloud"}, {"model": "qwen3.6:latest"},
{"think": "high"}, {"stream": True}, {"images": ["anything"]},
{"tools": []}, {"prompt": "x" * 16384}, {"format": "text"},
{"options": {"num_gpu": 999}}, {"prompt": None},
])
def test_rejected_requests_do_not_reach_upstream(payload, upstream, change):
payload.update(change)
assert api.generate(json.dumps(payload))[0] == 400
assert upstream == []
@pytest.mark.parametrize("key,value", [("temperature", float("nan")), ("num_predict", -1),
("num_ctx", 262144), ("seed", True)])
def test_invalid_options_do_not_reach_upstream(payload, upstream, key, value):
payload["options"][key] = value
assert api.generate(json.dumps(payload))[0] == 400
assert upstream == []
@pytest.mark.parametrize("mode", ["version", "digest", "missing", "outage"])
def test_upstream_identity_failure_never_sends_source(payload, monkeypatch, mode):
calls = []
def broken(path, *args, **kwargs):
calls.append(path)
if mode == "outage":
raise URLError("unavailable")
if path == "/api/version":
return {"version": "changed" if mode == "version" else api.VERSION}
return {"models": [] if mode == "missing" else [{"name": n, "digest": "wrong"} for n in api.PINS]}
monkeypatch.setattr(api, "_request", broken)
assert api.generate(json.dumps(payload))[0] == 503
assert "/api/chat" not in calls
assert api.models_response()[0] == 503
def test_busy_request_and_truncated_generation_are_not_success(payload, upstream, monkeypatch):
api._inference.acquire()
try:
assert api.generate(json.dumps(payload))[0] == 429
assert not upstream
finally:
api._inference.release()
original = api._request
def truncated(path, *args, **kwargs):
result = original(path, *args, **kwargs)
if path == "/api/chat":
result["done_reason"] = "length"
return result
monkeypatch.setattr(api, "_request", truncated)
assert api.generate(json.dumps(payload))[0] == 422
def test_redirects_are_never_followed():
assert api.NoRedirect().redirect_request(None, None, 307, "redirect", {}, "https://external.invalid") is None
def test_thinking_is_never_promoted_to_missing_final_content(payload, upstream, monkeypatch):
original = api._request
def thinking_only(path, *args, **kwargs):
result = original(path, *args, **kwargs)
if path == "/api/chat":
result["message"] = {"thinking": '{"ok":true}', "content": ""}
return result
monkeypatch.setattr(api, "_request", thinking_only)
status, result = api.generate(json.dumps(payload))
assert status == 422
assert "thinking" not in result and "response" not in result