Files
VoiceStudio/backend/tests/test_runtime_adapter_execute.py
T
velixio 4335c8c1ea 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.
2026-08-13 13:51:14 +05:30

332 lines
13 KiB
Python

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