"""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