test(runtime-adapter): cover the preflight contract and execute taxonomy

Fake-engine/fake-inventory tests over a real UDS gRPC server plus a fast
direct-executor path: health/capabilities shape and version identity, the
Go-preflight port passing with a READY model and failing closed without
one, only-READY-counts semantics, digest stability/sensitivity, execute
happy path (manifest checksum matches the written WAV), deadline
enforcement, cancel race with idempotent dispositions, slot exhaustion,
duplicate attempts, URL/relative handle rejection, checksum mismatch, and
the input/model-load/inference/GPU/storage failure classification.
This commit is contained in:
velixio
2026-08-13 13:51:14 +05:30
parent dcd8683f3a
commit 4335c8c1ea
3 changed files with 697 additions and 0 deletions
+205
View File
@@ -0,0 +1,205 @@
"""Shared fakes and harness for the runtime-adapter tests.
Not a test module (no ``test_`` prefix): imported by
``test_runtime_adapter_capabilities.py`` and
``test_runtime_adapter_execute.py``.
"""
from __future__ import annotations
import contextlib
import hashlib
import os
import shutil
import tempfile
import threading
import time
import grpc
from runtime_adapter.gen import runtime_adapter_pb2 as pb2
from runtime_adapter.gen import runtime_adapter_pb2_grpc as pb2_grpc
from runtime_adapter.inventory import (
STATE_READY,
DeviceInfo,
ModelInfo,
)
from runtime_adapter.server import RuntimeContext, create_server
READY_MODEL = ModelInfo(
catalog_model_id="fake-tts",
model_version="a" * 40,
model_digest="sha256:" + "b" * 64,
precisions=("fp32",),
features=("tts",),
state=STATE_READY,
)
DEVICE = DeviceInfo(
device_id="cpu:0",
hardware_class="test-cpu",
total_vram_bytes=8 * 1024**3,
total_slots=1,
free_slots=1,
)
class FakeInventory:
def __init__(self, models=None, devices=None):
self._models = list(models) if models is not None else [READY_MODEL]
self._devices = list(devices) if devices is not None else [DEVICE]
def devices(self, busy_slots: int = 0):
return [
DeviceInfo(
device_id=d.device_id,
hardware_class=d.hardware_class,
total_vram_bytes=d.total_vram_bytes,
total_slots=d.total_slots,
free_slots=max(0, d.total_slots - busy_slots),
)
for d in self._devices
]
def models(self):
return list(self._models)
class FakeEngine:
"""Half a second of silence at 24 kHz, instantly."""
sample_rate = 24000
def __init__(self):
self.generate_calls = []
def ensure_ready(self):
pass
def generate(self, text, **kw):
import torch
self.generate_calls.append((text, kw))
return torch.zeros(1, 12000)
class SlowEngine(FakeEngine):
"""Sleeps through generate in small slices so tests stay responsive."""
def __init__(self, seconds: float = 10.0):
super().__init__()
self.seconds = seconds
self.started = threading.Event()
def generate(self, text, **kw):
self.started.set()
deadline = time.monotonic() + self.seconds
while time.monotonic() < deadline:
time.sleep(0.01)
return super().generate(text, **kw)
class FailingEngine(FakeEngine):
def __init__(self, exc: BaseException, phase: str = "synthesis"):
super().__init__()
self._exc = exc
self._phase = phase
def ensure_ready(self):
if self._phase == "model_load":
raise self._exc
def generate(self, text, **kw):
raise self._exc
def make_context(engine=None, inventory=None, **kw) -> RuntimeContext:
engine = engine if engine is not None else FakeEngine()
engines = {READY_MODEL.catalog_model_id: engine}
kw.setdefault("progress_interval", 0.05)
kw.setdefault("poll_interval", 0.005)
return RuntimeContext(
runtime_version="1.2.3-test",
inventory=inventory if inventory is not None else FakeInventory(),
engine_provider=lambda model_id: engines[model_id],
**kw,
)
@contextlib.contextmanager
def serve_over_socket(context: RuntimeContext, tmp_path=None):
# A pytest tmp_path routinely exceeds the 103-character Unix-socket path
# limit on macOS, so the socket gets its own short private tempdir.
socket_dir = tempfile.mkdtemp(prefix="vs-rta-")
socket_path = os.path.join(socket_dir, "runtime.sock")
server = create_server(context, socket_path)
server.start()
channel = grpc.insecure_channel(f"unix:{socket_path}")
try:
yield pb2_grpc.RuntimeAdapterServiceStub(channel), socket_path
finally:
channel.close()
server.stop(grace=0).wait()
shutil.rmtree(socket_dir, ignore_errors=True)
def make_execute_request(
tmp_path,
text: str = "hello runtime",
*,
attempt_id: str = "attempt-1",
job_id: str = "job-1",
model: ModelInfo = READY_MODEL,
device_id: str = "cpu:0",
deadline_in_s: float = 30.0,
parameters: dict | None = None,
input_sha256: str | None = None,
input_handle: str | None = None,
output_handle: str | None = None,
) -> pb2.ExecuteRequest:
if input_handle is None:
input_path = tmp_path / "input.txt"
input_path.write_text(text, encoding="utf-8")
input_handle = str(input_path)
if input_sha256 is None and text is not None:
input_sha256 = hashlib.sha256(text.encode("utf-8")).hexdigest()
if output_handle is None:
output_handle = str(tmp_path / "output.wav")
return pb2.ExecuteRequest(
job_id=job_id,
attempt_id=attempt_id,
device_id=device_id,
slot_id="slot-0",
model=pb2.ModelSpec(
catalog_model_id=model.catalog_model_id,
model_version=model.model_version,
model_digest=model.model_digest,
precision="fp32",
),
parameters=parameters or {},
inputs=[
pb2.LocalArtifact(
artifact_id="in-1",
local_handle=input_handle,
operation=pb2.LOCAL_ARTIFACT_OPERATION_READ,
expected_sha256=input_sha256 or "",
media_type="text/plain",
)
],
outputs=[
pb2.LocalArtifact(
artifact_id="out-1",
local_handle=output_handle,
operation=pb2.LOCAL_ARTIFACT_OPERATION_WRITE,
media_type="audio/wav",
)
],
deadline_unix_ms=int((time.time() + deadline_in_s) * 1000),
maximum_preview_bytes=0,
)
def terminal_of(events):
last = events[-1].event
kind = last.WhichOneof("payload")
assert kind in ("completed", "failed", "canceled"), kind
return kind, last
@@ -0,0 +1,161 @@
"""Health/GetCapabilities shape, preflight parity, digest stability.
Mirrors what ``internal/gateway/preflight.go`` in vssaas enforces: READY
health with version evidence, identical versions across both calls, valid
unique devices, and only explicitly-READY models counting as schedulable.
"""
from __future__ import annotations
import os
import sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _runtime_adapter_helpers import ( # noqa: E402
DEVICE,
READY_MODEL,
FakeInventory,
make_context,
serve_over_socket,
)
from runtime_adapter.digest import file_sha256, snapshot_digest
from runtime_adapter.gen import runtime_adapter_pb2 as pb2
from runtime_adapter.inventory import (
STATE_FAILED,
STATE_INSTALLED,
STATE_LOADING,
ModelInfo,
)
from runtime_adapter.selfcheck import PreflightError, run_preflight
from runtime_adapter.server import prepare_socket
def _model(state, model_id="other-model", digest="sha256:" + "c" * 64):
return ModelInfo(
catalog_model_id=model_id,
model_version="d" * 40,
model_digest=digest,
precisions=("fp32",),
features=("tts",),
state=state,
)
def test_health_and_capabilities_versions_are_identical_and_ready(tmp_path):
with serve_over_socket(make_context(), tmp_path) as (stub, _):
health = stub.Health(pb2.HealthRequest(), timeout=5)
caps = stub.GetCapabilities(pb2.GetCapabilitiesRequest(), timeout=5)
assert health.state == pb2.SERVING_STATE_READY
assert health.runtime_version == "1.2.3-test"
assert health.adapter_version.strip()
assert caps.runtime_version == health.runtime_version
assert caps.adapter_version == health.adapter_version
def test_capabilities_report_device_and_ready_model_evidence(tmp_path):
inventory = FakeInventory(
models=[
READY_MODEL,
_model(STATE_LOADING, "loading-model"),
_model(STATE_FAILED, "failed-model"),
_model(STATE_INSTALLED, "installed-model"),
]
)
with serve_over_socket(make_context(inventory=inventory), tmp_path) as (stub, _):
caps = stub.GetCapabilities(pb2.GetCapabilitiesRequest(), timeout=5)
[device] = caps.devices
assert device.device_id == DEVICE.device_id
assert device.hardware_class == DEVICE.hardware_class
assert device.total_vram_bytes > 0
assert 0 < device.free_slots <= device.total_slots
by_id = {model.catalog_model_id: model for model in caps.models}
ready = by_id[READY_MODEL.catalog_model_id]
assert ready.state == pb2.RUNTIME_MODEL_STATE_READY
assert len(ready.model_version) == 40
assert ready.model_digest.startswith("sha256:")
assert list(ready.precisions)
# A loading/failed/installed model is reported truthfully, never READY.
assert by_id["loading-model"].state == pb2.RUNTIME_MODEL_STATE_LOADING
assert by_id["failed-model"].state == pb2.RUNTIME_MODEL_STATE_FAILED
assert by_id["installed-model"].state == pb2.RUNTIME_MODEL_STATE_INSTALLED
def test_preflight_port_passes_against_a_ready_server(tmp_path):
inventory = FakeInventory(models=[READY_MODEL, _model(STATE_LOADING)])
with serve_over_socket(make_context(inventory=inventory), tmp_path) as (
stub,
socket_path,
):
summary = run_preflight(socket_path, timeout_s=5)
assert summary.ready_model_count == 1 # the loading model must not count
assert summary.device_count == 1
assert summary.runtime_version == "1.2.3-test"
assert summary.total_slots == 1
def test_preflight_fails_closed_without_a_ready_model(tmp_path):
inventory = FakeInventory(models=[_model(STATE_LOADING)])
with serve_over_socket(make_context(inventory=inventory), tmp_path) as (
stub,
socket_path,
):
health = stub.Health(pb2.HealthRequest(), timeout=5)
assert health.state == pb2.SERVING_STATE_DEGRADED
assert "no-ready-model" in health.health_flags
with pytest.raises(PreflightError):
run_preflight(socket_path, timeout_s=5)
def test_prepare_socket_rejects_unsafe_paths(tmp_path):
with pytest.raises(ValueError):
prepare_socket("relative/socket.sock")
regular = tmp_path / "not-a-socket"
regular.write_text("x")
with pytest.raises(ValueError):
prepare_socket(str(regular))
missing_parent = tmp_path / "nope" / "runtime.sock"
with pytest.raises(ValueError):
prepare_socket(str(missing_parent))
def test_snapshot_digest_is_stable_and_content_sensitive(tmp_path):
snapshot = tmp_path / "snapshots" / "rev"
snapshot.mkdir(parents=True)
(snapshot / "weights.bin").write_bytes(b"\x01\x02\x03")
(snapshot / "config.json").write_text("{}")
cache = tmp_path / "digest-cache.json"
first = snapshot_digest(snapshot, cache_path=cache)
second = snapshot_digest(snapshot, cache_path=cache) # served from cache
assert first == second
assert first.startswith("sha256:")
assert cache.exists()
# Any byte change must change the digest (cache invalidated by mtime/size).
(snapshot / "weights.bin").write_bytes(b"\x01\x02\x04")
assert snapshot_digest(snapshot, cache_path=cache) != first
with pytest.raises(FileNotFoundError):
snapshot_digest(tmp_path / "empty-none")
def test_file_sha256_matches_hashlib(tmp_path):
import hashlib
payload = b"runtime adapter"
path = tmp_path / "f.bin"
path.write_bytes(payload)
assert file_sha256(path) == hashlib.sha256(payload).hexdigest()
def test_socket_file_is_private_to_the_node(tmp_path):
import stat
with serve_over_socket(make_context(), tmp_path) as (_stub, socket_path):
mode = os.lstat(socket_path).st_mode
assert stat.S_ISSOCK(mode)
@@ -0,0 +1,331 @@
"""Execute/Cancel: happy path, deadline, cancel race, failure taxonomy."""
from __future__ import annotations
import hashlib
import os
import sys
import threading
import time
import pytest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _runtime_adapter_helpers import ( # noqa: E402
READY_MODEL,
FailingEngine,
FakeInventory,
SlowEngine,
make_context,
make_execute_request,
serve_over_socket,
terminal_of,
)
from runtime_adapter import codes
from runtime_adapter.gen import runtime_adapter_pb2 as pb2
from runtime_adapter.inventory import STATE_INSTALLED, ModelInfo
def _run_direct(context, request):
"""Drive the executor without a live gRPC server (fast path for taxonomy)."""
return list(context.executor().execute(request, None))
def _failure(events):
kind, last = terminal_of(events)
assert kind == "failed", f"expected failed terminal, got {kind}"
return last.failed
# ── happy path ────────────────────────────────────────────────────────────
def test_execute_happy_path_streams_and_writes_the_manifest(tmp_path):
context = make_context()
request = make_execute_request(tmp_path, text="hello runtime")
with serve_over_socket(context, tmp_path) as (stub, _):
events = list(stub.Execute(request, timeout=30))
payloads = [event.event.WhichOneof("payload") for event in events]
assert payloads[0] == "started"
assert payloads[-1] == "completed"
assert all(kind == "progress" for kind in payloads[1:-1])
sequences = [event.event.sequence for event in events]
assert sequences == sorted(sequences)
assert all(event.event.attempt_id == "attempt-1" for event in events)
completed = events[-1].event.completed
[manifest] = completed.outputs
output_path = tmp_path / "output.wav"
assert manifest.local_handle == str(output_path)
assert output_path.stat().st_size == manifest.size_bytes > 0
assert manifest.sha256 == hashlib.sha256(output_path.read_bytes()).hexdigest()
assert manifest.media_type == "audio/wav"
assert manifest.duration_ms == 500 # 12000 samples at 24 kHz
measurements = completed.measurements
assert measurements.normalized_input_characters == len("hello runtime")
assert measurements.output_audio_ms == 500
def test_execute_passes_typed_parameters_to_the_engine(tmp_path):
from _runtime_adapter_helpers import FakeEngine
engine = FakeEngine()
context = make_context(engine=engine)
request = make_execute_request(
tmp_path,
parameters={
"speed": pb2.ParameterValue(number_value=1.5),
"language": pb2.ParameterValue(string_value="en"),
"num_step": pb2.ParameterValue(integer_value=8),
},
)
events = _run_direct(context, request)
assert terminal_of(events)[0] == "completed"
[(text, kwargs)] = engine.generate_calls
assert text == "hello runtime"
assert kwargs == {"speed": 1.5, "language": "en", "num_step": 8}
# ── deadline ──────────────────────────────────────────────────────────────
def test_deadline_is_enforced_with_a_stable_code(tmp_path):
context = make_context(engine=SlowEngine(seconds=30))
request = make_execute_request(tmp_path, deadline_in_s=0.4)
start = time.monotonic()
events = _run_direct(context, request)
elapsed = time.monotonic() - start
failed = _failure(events)
assert failed.stable_code in (codes.INFERENCE_DEADLINE, codes.MODEL_LOAD_DEADLINE)
assert failed.failure_class in (
pb2.RUNTIME_FAILURE_CLASS_INFERENCE,
pb2.RUNTIME_FAILURE_CLASS_MODEL_LOAD,
)
assert elapsed < 5, "terminal event must arrive promptly after the deadline"
def test_deadline_in_the_past_is_invalid_input(tmp_path):
context = make_context()
request = make_execute_request(tmp_path)
request.deadline_unix_ms = int(time.time() * 1000) - 1000
failed = _failure(_run_direct(context, request))
assert failed.stable_code == codes.INPUT_DEADLINE_INVALID
assert failed.failure_class == pb2.RUNTIME_FAILURE_CLASS_INPUT
# ── cancel ────────────────────────────────────────────────────────────────
def test_cancel_race_yields_canceled_terminal_and_idempotent_dispositions(tmp_path):
engine = SlowEngine(seconds=30)
context = make_context(engine=engine)
request = make_execute_request(tmp_path)
with serve_over_socket(context, tmp_path) as (stub, _):
stream = stub.Execute(request, timeout=30)
first = next(stream)
assert first.event.WhichOneof("payload") == "started"
assert engine.started.wait(5), "engine must be mid-generate for the race"
cancel = pb2.CancelRequest(job_id="job-1", attempt_id="attempt-1")
assert stub.Cancel(cancel, timeout=5).disposition == (
pb2.CANCEL_DISPOSITION_ACCEPTED
)
# Idempotent while still running.
assert stub.Cancel(cancel, timeout=5).disposition == (
pb2.CANCEL_DISPOSITION_ACCEPTED
)
events = [first, *stream]
kind, last = terminal_of(events)
assert kind == "canceled"
assert last.canceled.HasField("measurements")
# After the terminal event the same cancel is ALREADY_TERMINAL …
assert stub.Cancel(cancel, timeout=5).disposition == (
pb2.CANCEL_DISPOSITION_ALREADY_TERMINAL
)
# … and an unknown attempt is NOT_FOUND.
unknown = pb2.CancelRequest(job_id="job-1", attempt_id="nope")
assert stub.Cancel(unknown, timeout=5).disposition == (
pb2.CANCEL_DISPOSITION_NOT_FOUND
)
def test_cancel_before_any_execute_is_not_found(tmp_path):
with serve_over_socket(make_context(), tmp_path) as (stub, _):
response = stub.Cancel(
pb2.CancelRequest(job_id="j", attempt_id="never-ran"), timeout=5
)
assert response.disposition == pb2.CANCEL_DISPOSITION_NOT_FOUND
# ── failure classification ────────────────────────────────────────────────
def test_model_load_failure_is_classified(tmp_path):
engine = FailingEngine(RuntimeError("weights corrupted"), phase="model_load")
failed = _failure(
_run_direct(make_context(engine=engine), make_execute_request(tmp_path))
)
assert failed.stable_code == codes.MODEL_LOAD_FAILED
assert failed.failure_class == pb2.RUNTIME_FAILURE_CLASS_MODEL_LOAD
def test_inference_failure_is_classified(tmp_path):
engine = FailingEngine(ValueError("synthesis exploded"))
failed = _failure(
_run_direct(make_context(engine=engine), make_execute_request(tmp_path))
)
assert failed.stable_code == codes.INFERENCE_FAILED
assert failed.failure_class == pb2.RUNTIME_FAILURE_CLASS_INFERENCE
def test_gpu_oom_is_classified_as_gpu_resource(tmp_path):
engine = FailingEngine(RuntimeError("CUDA out of memory. Tried to allocate…"))
failed = _failure(
_run_direct(make_context(engine=engine), make_execute_request(tmp_path))
)
assert failed.stable_code == codes.GPU_OUT_OF_MEMORY
assert failed.failure_class == pb2.RUNTIME_FAILURE_CLASS_GPU_RESOURCE
def test_engine_input_rejection_is_invalid_input(tmp_path):
from services.tts_backend import TTSInputError
engine = FailingEngine(TTSInputError("text too long for this engine"))
failed = _failure(
_run_direct(make_context(engine=engine), make_execute_request(tmp_path))
)
assert failed.stable_code == codes.INPUT_REJECTED
assert failed.failure_class == pb2.RUNTIME_FAILURE_CLASS_INPUT
def test_url_handles_are_rejected_never_fetched(tmp_path):
request = make_execute_request(
tmp_path, input_handle="https://evil.example/input.txt", input_sha256=""
)
failed = _failure(_run_direct(make_context(), request))
assert failed.stable_code == codes.INPUT_HANDLE_INVALID
assert failed.failure_class == pb2.RUNTIME_FAILURE_CLASS_INPUT
def test_relative_output_handle_is_rejected(tmp_path):
request = make_execute_request(tmp_path, output_handle="relative/out.wav")
failed = _failure(_run_direct(make_context(), request))
assert failed.stable_code == codes.INPUT_HANDLE_INVALID
def test_model_digest_mismatch_is_rejected(tmp_path):
request = make_execute_request(tmp_path)
request.model.model_digest = "sha256:" + "f" * 64
failed = _failure(_run_direct(make_context(), request))
assert failed.stable_code == codes.INPUT_MODEL_DIGEST_MISMATCH
def test_non_ready_model_is_rejected(tmp_path):
installed = ModelInfo(
catalog_model_id=READY_MODEL.catalog_model_id,
model_version=READY_MODEL.model_version,
model_digest=READY_MODEL.model_digest,
precisions=READY_MODEL.precisions,
features=READY_MODEL.features,
state=STATE_INSTALLED,
)
context = make_context(inventory=FakeInventory(models=[installed]))
failed = _failure(_run_direct(context, make_execute_request(tmp_path)))
assert failed.stable_code == codes.INPUT_MODEL_NOT_READY
def test_unknown_model_is_rejected(tmp_path):
request = make_execute_request(tmp_path)
request.model.catalog_model_id = "who-dis"
failed = _failure(_run_direct(make_context(), request))
assert failed.stable_code == codes.INPUT_MODEL_UNKNOWN
def test_unknown_and_out_of_range_parameters_are_rejected(tmp_path):
unknown = make_execute_request(
tmp_path,
parameters={"exfiltrate": pb2.ParameterValue(string_value="x")},
)
assert _failure(_run_direct(make_context(), unknown)).stable_code == (
codes.INPUT_PARAMETER_UNKNOWN
)
out_of_range = make_execute_request(
tmp_path,
attempt_id="attempt-2",
parameters={"speed": pb2.ParameterValue(number_value=99.0)},
)
assert _failure(_run_direct(make_context(), out_of_range)).stable_code == (
codes.INPUT_PARAMETER_RANGE
)
def test_input_checksum_mismatch_is_rejected(tmp_path):
request = make_execute_request(tmp_path, input_sha256="0" * 64)
failed = _failure(_run_direct(make_context(), request))
assert failed.stable_code == codes.INPUT_CHECKSUM_MISMATCH
def test_empty_text_is_rejected(tmp_path):
request = make_execute_request(tmp_path, text=" ")
failed = _failure(_run_direct(make_context(), request))
assert failed.stable_code == codes.INPUT_TEXT_EMPTY
def test_unwritable_output_directory_is_local_storage(tmp_path):
locked = tmp_path / "locked"
locked.mkdir()
request = make_execute_request(tmp_path, output_handle=str(locked / "out.wav"))
locked.chmod(0o500)
try:
failed = _failure(_run_direct(make_context(), request))
finally:
locked.chmod(0o700)
assert failed.stable_code == codes.STORAGE_WRITE_FAILED
assert failed.failure_class == pb2.RUNTIME_FAILURE_CLASS_LOCAL_STORAGE
def test_duplicate_attempt_id_is_rejected(tmp_path):
context = make_context()
executor = context.executor()
first = make_execute_request(tmp_path)
assert terminal_of(list(executor.execute(first, None)))[0] == "completed"
duplicate = make_execute_request(tmp_path)
events = list(executor.execute(duplicate, None))
failed = _failure(events)
assert failed.stable_code == codes.INPUT_ATTEMPT_DUPLICATE
def test_slot_exhaustion_is_gpu_resource(tmp_path):
engine = SlowEngine(seconds=30)
context = make_context(engine=engine, slot_limit=1)
executor = context.executor()
hog = make_execute_request(tmp_path, attempt_id="hog")
hog_events = []
hog_thread = threading.Thread(
target=lambda: hog_events.extend(executor.execute(hog, None)), daemon=True
)
hog_thread.start()
assert engine.started.wait(5)
try:
crowded = make_execute_request(tmp_path, attempt_id="crowded")
failed = _failure(list(executor.execute(crowded, None)))
assert failed.stable_code == codes.GPU_SLOTS_EXHAUSTED
assert failed.failure_class == pb2.RUNTIME_FAILURE_CLASS_GPU_RESOURCE
finally:
context.registry.cancel("job-1", "hog")
hog_thread.join(timeout=10)
assert terminal_of(hog_events)[0] == "canceled"
def test_safe_detail_never_carries_local_paths(tmp_path):
engine = FailingEngine(RuntimeError(f"failed loading {tmp_path}/weights.bin"))
failed = _failure(
_run_direct(make_context(engine=engine), make_execute_request(tmp_path))
)
assert str(tmp_path) not in failed.safe_detail
assert "<path>" in failed.safe_detail