416 lines
13 KiB
Python
416 lines
13 KiB
Python
"""Release gates for bounded, advisory-only voice route preflight."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
import threading
|
|
import time
|
|
from http.server import ThreadingHTTPServer
|
|
from types import SimpleNamespace
|
|
from urllib.error import HTTPError
|
|
from urllib.request import Request, urlopen
|
|
|
|
import pytest
|
|
from testing.tests.test_hermes_chat_support import ROOT
|
|
|
|
|
|
HERMES = ROOT / "services" / "hermes"
|
|
|
|
|
|
def _load_runtime(monkeypatch):
|
|
"""Load the sibling runtime modules without requiring httpx in image CI."""
|
|
monkeypatch.setitem(sys.modules, "httpx", SimpleNamespace())
|
|
preflight_path = HERMES / "scripts" / "voice_route_preflight.py"
|
|
spec = importlib.util.spec_from_file_location("voice_route_preflight", preflight_path)
|
|
assert spec and spec.loader
|
|
preflight = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(preflight)
|
|
monkeypatch.setitem(sys.modules, "voice_route_preflight", preflight)
|
|
broker_path = HERMES / "scripts" / "classifier_broker.py"
|
|
spec = importlib.util.spec_from_file_location("classifier_broker", broker_path)
|
|
assert spec and spec.loader
|
|
broker = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(broker)
|
|
return preflight, broker
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"payload",
|
|
[
|
|
{},
|
|
{"turn_id": "bad/id", "revision": 1, "transcript": "long enough text"},
|
|
{"turn_id": "nonce-1", "revision": True, "transcript": "long enough text"},
|
|
{"turn_id": "nonce-1", "revision": 0, "transcript": "long enough text"},
|
|
{"turn_id": "nonce-1", "revision": 1, "transcript": None},
|
|
{"turn_id": "nonce-1", "revision": 1, "transcript": "short"},
|
|
{"turn_id": "nonce-1", "revision": 1, "transcript": "x" * 513},
|
|
],
|
|
)
|
|
def test_preflight_input_bounds_fail_closed(monkeypatch, payload):
|
|
preflight, _ = _load_runtime(monkeypatch)
|
|
with pytest.raises(ValueError):
|
|
preflight.validate_request(payload)
|
|
|
|
|
|
def test_preflight_is_same_14b_model_and_never_hosts_a_foreground_turn(monkeypatch):
|
|
preflight, _ = _load_runtime(monkeypatch)
|
|
payload = preflight.inference_payload("Please compare these two safe approaches")
|
|
|
|
assert preflight.MODEL == "qwen2.5:14b-instruct-q4_0"
|
|
assert payload["model"] == preflight.MODEL
|
|
assert payload["stream"] is True
|
|
assert payload["max_tokens"] <= 16
|
|
assert payload["temperature"] == 0
|
|
assert "tools" not in payload
|
|
assert "provider" not in payload
|
|
assert preflight.TIMEOUT_SECONDS <= 0.75
|
|
|
|
|
|
def test_preflight_is_once_per_random_browser_turn_and_authority_preempts(monkeypatch):
|
|
preflight, _ = _load_runtime(monkeypatch)
|
|
coordinator = preflight.Coordinator()
|
|
cancel = coordinator.begin("8f" * 16 + "-1-1")
|
|
assert cancel is not None
|
|
|
|
class Resource:
|
|
closed = False
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
client = Resource()
|
|
response = Resource()
|
|
assert coordinator.register(cancel, client=client, response=response)
|
|
started = time.monotonic()
|
|
coordinator.begin_authoritative()
|
|
assert time.monotonic() - started < 0.1
|
|
assert cancel.is_set()
|
|
assert client.closed and response.closed
|
|
assert coordinator.begin("9e" * 16 + "-1-1") is None
|
|
coordinator.end_authoritative()
|
|
coordinator.end()
|
|
# A completed browser turn cannot spend another 750 ms on a later partial.
|
|
assert coordinator.begin("8f" * 16 + "-1-1") is None
|
|
|
|
|
|
def test_coordinator_prunes_seen_turns_and_tolerates_close_failures(monkeypatch):
|
|
preflight, _ = _load_runtime(monkeypatch)
|
|
coordinator = preflight.Coordinator()
|
|
now = time.monotonic()
|
|
coordinator.seen = {f"turn-{index}": now for index in range(300)}
|
|
coordinator._prune(now)
|
|
assert len(coordinator.seen) == preflight.MAX_SEEN_TURNS
|
|
|
|
class BrokenResource:
|
|
def close(self):
|
|
raise RuntimeError("close failed")
|
|
|
|
cancel = coordinator.begin("fresh-turn")
|
|
assert cancel is not None
|
|
assert coordinator.register(cancel, client=BrokenResource(), response=BrokenResource())
|
|
coordinator.begin_authoritative()
|
|
assert cancel.is_set()
|
|
coordinator.end_authoritative()
|
|
coordinator.end_authoritative()
|
|
coordinator.end()
|
|
|
|
unrelated = threading.Event()
|
|
assert coordinator.register(unrelated) is False
|
|
assert unrelated.is_set()
|
|
|
|
idle = preflight.Coordinator()
|
|
idle.begin_authoritative()
|
|
idle.end_authoritative()
|
|
|
|
|
|
def _stream_with_fake_httpx(
|
|
monkeypatch,
|
|
preflight,
|
|
coordinator,
|
|
lines,
|
|
*,
|
|
fail_build=False,
|
|
close_raises=False,
|
|
preempt_on_send=False,
|
|
):
|
|
"""Drive the streamed parser and cleanup branches without network I/O."""
|
|
|
|
class Response:
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def iter_lines(self):
|
|
yield from lines
|
|
|
|
def close(self):
|
|
if close_raises:
|
|
raise RuntimeError("response close failed")
|
|
|
|
response = Response()
|
|
|
|
class Client:
|
|
def __init__(self, timeout):
|
|
self.timeout = timeout
|
|
|
|
def build_request(self, *args, **kwargs):
|
|
if fail_build:
|
|
raise RuntimeError("build failed")
|
|
return (args, kwargs)
|
|
|
|
def send(self, request, stream=False):
|
|
assert stream is True
|
|
if preempt_on_send:
|
|
coordinator.begin_authoritative()
|
|
return response
|
|
|
|
def close(self):
|
|
if close_raises:
|
|
raise RuntimeError("client close failed")
|
|
|
|
monkeypatch.setattr(
|
|
preflight,
|
|
"httpx",
|
|
SimpleNamespace(Timeout=lambda *args, **kwargs: (args, kwargs), Client=Client),
|
|
)
|
|
cancel = coordinator.begin("stream-" + str(time.monotonic_ns()))
|
|
assert cancel is not None
|
|
result = coordinator.stream("This provisional request is stable", cancel)
|
|
if preempt_on_send:
|
|
coordinator.end_authoritative()
|
|
coordinator.end()
|
|
return result
|
|
|
|
|
|
def test_stream_parser_accepts_only_valid_bounded_json(monkeypatch):
|
|
preflight, _ = _load_runtime(monkeypatch)
|
|
coordinator = preflight.Coordinator()
|
|
content = json.dumps({"tier": "deep"})
|
|
lines = [
|
|
"ignored",
|
|
"data: not-json",
|
|
'data: {"choices":[]}',
|
|
'data: {"choices":[{"delta":{"content":7}}]}',
|
|
"data: "
|
|
+ json.dumps({"choices": [{"delta": {"content": content}}]}),
|
|
"data: [DONE]",
|
|
]
|
|
assert _stream_with_fake_httpx(monkeypatch, preflight, coordinator, lines) == "deep"
|
|
assert 'outcome="success"} 1' in coordinator.metrics()
|
|
|
|
invalid = preflight.Coordinator()
|
|
lines = [
|
|
"data: "
|
|
+ json.dumps(
|
|
{"choices": [{"delta": {"content": json.dumps({"tier": "hosted"})}}]}
|
|
)
|
|
]
|
|
assert _stream_with_fake_httpx(monkeypatch, preflight, invalid, lines) == ""
|
|
assert 'outcome="failure"} 1' in invalid.metrics()
|
|
|
|
|
|
def test_stream_fails_closed_on_oversize_preemption_and_cleanup_errors(monkeypatch):
|
|
preflight, _ = _load_runtime(monkeypatch)
|
|
oversized = preflight.Coordinator()
|
|
line = "data: " + json.dumps({"choices": [{"delta": {"content": "x" * 129}}]})
|
|
assert _stream_with_fake_httpx(monkeypatch, preflight, oversized, [line]) == ""
|
|
assert 'outcome="cancelled"} 1' in oversized.metrics()
|
|
|
|
preempted = preflight.Coordinator()
|
|
assert (
|
|
_stream_with_fake_httpx(
|
|
monkeypatch, preflight, preempted, [], preempt_on_send=True
|
|
)
|
|
== ""
|
|
)
|
|
assert 'outcome="cancelled"} 1' in preempted.metrics()
|
|
|
|
failed = preflight.Coordinator()
|
|
assert (
|
|
_stream_with_fake_httpx(
|
|
monkeypatch,
|
|
preflight,
|
|
failed,
|
|
[],
|
|
fail_build=True,
|
|
close_raises=True,
|
|
)
|
|
== ""
|
|
)
|
|
assert 'outcome="failure"} 1' in failed.metrics()
|
|
|
|
|
|
def test_stream_rejects_a_cancelled_reservation_before_network(monkeypatch):
|
|
preflight, _ = _load_runtime(monkeypatch)
|
|
coordinator = preflight.Coordinator()
|
|
cancel = coordinator.begin("cancel-before-network")
|
|
assert cancel is not None
|
|
cancel.set()
|
|
closed = {"value": False}
|
|
|
|
class Client:
|
|
def __init__(self, timeout):
|
|
self.timeout = timeout
|
|
|
|
def close(self):
|
|
closed["value"] = True
|
|
|
|
monkeypatch.setattr(
|
|
preflight,
|
|
"httpx",
|
|
SimpleNamespace(Timeout=lambda *args, **kwargs: None, Client=Client),
|
|
)
|
|
assert coordinator.stream("This request will be cancelled", cancel) == ""
|
|
assert closed["value"] is True
|
|
assert 'outcome="cancelled"} 1' in coordinator.metrics()
|
|
coordinator.end()
|
|
|
|
|
|
def test_absolute_deadline_closes_a_stalled_stream(monkeypatch):
|
|
preflight, _ = _load_runtime(monkeypatch)
|
|
monkeypatch.setattr(preflight, "TIMEOUT_SECONDS", 0.05)
|
|
closed = threading.Event()
|
|
|
|
class Response:
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def iter_lines(self):
|
|
closed.wait(1)
|
|
if False:
|
|
yield ""
|
|
|
|
def close(self):
|
|
closed.set()
|
|
|
|
response = Response()
|
|
|
|
class Client:
|
|
def __init__(self, timeout):
|
|
self.timeout = timeout
|
|
|
|
def build_request(self, *args, **kwargs):
|
|
return (args, kwargs)
|
|
|
|
def send(self, request, stream=False):
|
|
assert stream is True
|
|
return response
|
|
|
|
def close(self):
|
|
response.close()
|
|
|
|
monkeypatch.setattr(
|
|
preflight,
|
|
"httpx",
|
|
SimpleNamespace(Timeout=lambda *args, **kwargs: (args, kwargs), Client=Client),
|
|
)
|
|
coordinator = preflight.Coordinator()
|
|
cancel = coordinator.begin("ab" * 16 + "-2-1")
|
|
assert cancel is not None
|
|
started = time.monotonic()
|
|
assert coordinator.stream("This provisional request is stable", cancel) == ""
|
|
elapsed = time.monotonic() - started
|
|
coordinator.end()
|
|
|
|
assert elapsed < 0.25
|
|
assert 'outcome="timeout"} 1' in coordinator.metrics()
|
|
|
|
|
|
def test_preflight_is_warm_only_and_final_payload_semantics_never_change(monkeypatch):
|
|
preflight, broker = _load_runtime(monkeypatch)
|
|
coordinator = preflight.Coordinator()
|
|
authoritative = {
|
|
"model": "qwen2.5:14b-instruct-q4_0",
|
|
"messages": [
|
|
{"role": "system", "content": "normal routing contract"},
|
|
{"role": "user", "content": "Please compare the two deployments"},
|
|
],
|
|
"response_format": {"type": "json_object"},
|
|
}
|
|
before = broker.compact_payload(authoritative)
|
|
cancel = coordinator.begin("aa" * 16 + "-2-1")
|
|
assert cancel is not None
|
|
coordinator.end()
|
|
after = broker.compact_payload(authoritative)
|
|
|
|
assert before == after
|
|
assert not hasattr(coordinator, "cache")
|
|
assert not hasattr(coordinator, "remember")
|
|
assert not hasattr(coordinator, "take_hint")
|
|
assert not hasattr(preflight, "add_advisory_hint")
|
|
|
|
|
|
def _post(server, payload):
|
|
request = Request(
|
|
f"http://127.0.0.1:{server.server_port}/voice/route-preflight",
|
|
data=json.dumps(payload).encode(),
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
with urlopen(request, timeout=2) as response:
|
|
return response.status, response.headers, response.read()
|
|
|
|
|
|
def test_handler_returns_exact_sanitized_binding_and_204_when_busy(monkeypatch):
|
|
preflight, broker = _load_runtime(monkeypatch)
|
|
coordinator = preflight.Coordinator()
|
|
monkeypatch.setattr(broker, "VOICE_PREFLIGHT", coordinator)
|
|
monkeypatch.setattr(coordinator, "stream", lambda transcript, cancel: "balanced")
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), broker.VoiceHandler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
payload = {
|
|
"turn_id": "cd" * 16 + "-3-1",
|
|
"revision": 4,
|
|
"transcript": "Private words that must never leave this boundary",
|
|
}
|
|
try:
|
|
status, headers, raw = _post(server, payload)
|
|
result = json.loads(raw)
|
|
assert status == 200
|
|
assert headers["Cache-Control"] == "no-store"
|
|
assert result == {
|
|
"turn_id": payload["turn_id"],
|
|
"revision": 4,
|
|
"tier": "balanced",
|
|
"target": "atlas/auto/balanced",
|
|
"advisory": True,
|
|
}
|
|
assert b"Private words" not in raw
|
|
status, headers, raw = _post(server, payload)
|
|
assert status == 204
|
|
assert headers["Cache-Control"] == "no-store"
|
|
assert raw == b""
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join(timeout=2)
|
|
|
|
|
|
def test_public_and_authoritative_listeners_have_disjoint_route_surfaces(monkeypatch):
|
|
_, broker = _load_runtime(monkeypatch)
|
|
|
|
def post_status(handler, path):
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
request = Request(
|
|
f"http://127.0.0.1:{server.server_port}{path}",
|
|
data=b"{}",
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urlopen(request, timeout=2) as response:
|
|
return response.status
|
|
except HTTPError as error:
|
|
return error.code
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join(timeout=2)
|
|
|
|
assert post_status(broker.Handler, "/voice/route-preflight") == 404
|
|
assert post_status(broker.VoiceHandler, "/v1/chat/completions") == 404
|