104 lines
3.8 KiB
Python
104 lines
3.8 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",
|
||
|
|
"response": '{"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/generate"
|
||
|
|
|
||
|
|
|
||
|
|
@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/generate" 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/generate":
|
||
|
|
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
|