The remote-GPU line, verified on hardware rather than asserted. **Dubbing renders on the worker.** dub_generate.py dispatches the coarse `dub_segments` operation through the gateway, following the audiobook pattern: per-unit local fallback after consecutive remote failures, one aggregated notice rather than one per segment. A 40-minute dub that loses its worker at segment 200 degrades instead of producing 200 error rows. **An out-of-date worker is now refused by name.** This was the worst defect in the plan and it was silent: an un-upgraded worker registered cleanly, then ignored `inputs` and rendered a clone with NO reference audio — returned as success. A plausible wrong result with nothing anywhere to surface it. Workers now declare features, and one missing them is turned away with the features named and `no task was run`. Verified live: a worker one commit behind was correctly refused. **"Offline" and "cannot run this" are different facts.** Asking a live worker for an engine it lacks answered "is offline or cannot be reached. Wake the selected worker" — while that worker reported ready, one free slot and 3.6 ms latency. The user was sent to wake a machine that was already awake. The scheduler now distinguishes absent from present-but- incapable, and names the engine rather than the operation, because the engine is the thing a user can install. **An engine with no catalog entry is no longer hidden.** A `repo_ids` non-emptiness check had been implemented as a runtime filter, so a worker silently refused to advertise any engine lacking a models.yaml entry — which is four registered engines, including CosyVoice. Users with those already installed would have lost remote support with only a log line. Empty `repo_ids` now means "not downloadable here", never "not runnable". **And a script so this stops being done by hand.** scripts/verify-remote-worker.sh runs the per-phase acceptance checks against a live worker, non-destructively. Its preconditions are the mistakes that cost the most time: exactly one listener on the control port (two instances silently shared it), and never detecting the worker with a pgrep pattern that matches the ssh shell running it. Its first real run found the dubbing picker claiming remote placement. That turned out to be the CHECK being stale, not the picker — the port had landed since it was written. It now asserts self-consistency instead: the picker may claim remote only for an operation the control plane actually advertises as remotely producible, which cannot rot the next time an op is ported. Backend 5291 passed, frontend 1812 passed. Acceptance script: no automated failures across Phases 4-8 on an RTX 4090. Four checks remain MANUAL by design — true airplane mode, concurrent downloads, killing a worker mid-audiobook, and the model-list UI — and are reported as unverified rather than passed.
633 lines
23 KiB
Python
633 lines
23 KiB
Python
"""Artifact transfer at the control-plane boundary.
|
|
|
|
The upload receiver is the one place where a remote peer writes bytes into the
|
|
user's filesystem and the app afterwards calls those bytes a finished render.
|
|
Every case here is a way that could go wrong without anybody noticing: a
|
|
transfer that stops early and is committed anyway, bytes that arrive out of
|
|
order and are appended regardless, a digest nobody checks, an artifact with no
|
|
ceiling, and — in the other direction — one worker reading the reference audio
|
|
staged for another's task.
|
|
|
|
The RPCs are driven directly rather than over a real stream: what is under test
|
|
is the integrity rule, not gRPC.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import sqlite3
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
from worker import deadlines as deadline_policy
|
|
from worker import identity, registry
|
|
from worker.clock import resolve
|
|
from worker.errors import ErrorClass, WorkerError
|
|
from worker.identity import WorkerKeypair
|
|
from worker.lifecycle import AttemptState, TaskState
|
|
from worker.pool import WorkerPool
|
|
from worker.protocol.gen import worker_v1_pb2 as pb
|
|
from worker.scheduler import Scheduler
|
|
from worker.transport import codec, server as server_module
|
|
from worker.transport.server import REQUIRED_FEATURES, SESSION_METADATA_KEY, WorkerServicer
|
|
|
|
ENGINE, MODEL, OP = "indextts", "IndexTTS-2", "tts"
|
|
|
|
|
|
@pytest.fixture
|
|
def db(tmp_path, monkeypatch):
|
|
"""Throwaway DB, patched where the stores actually read it."""
|
|
from worker import registry as reg
|
|
|
|
db_globals = reg.db_conn.__wrapped__.__globals__
|
|
path = str(tmp_path / "userdata.db")
|
|
with sqlite3.connect(path) as conn:
|
|
conn.executescript(db_globals["_BASE_SCHEMA"])
|
|
monkeypatch.setitem(db_globals, "DB_PATH", path)
|
|
return path
|
|
|
|
|
|
class _Aborted(Exception):
|
|
"""What a real gRPC ``context.abort`` does: it raises."""
|
|
|
|
def __init__(self, code, detail: str) -> None:
|
|
super().__init__(detail)
|
|
self.code = code
|
|
self.detail = detail
|
|
|
|
|
|
class _Context:
|
|
def __init__(self, token: str = "") -> None:
|
|
self.token = token
|
|
|
|
def peer(self) -> str:
|
|
return "ipv4:127.0.0.1:5555"
|
|
|
|
def invocation_metadata(self):
|
|
return ((SESSION_METADATA_KEY, self.token),) if self.token else ()
|
|
|
|
async def abort(self, code, detail):
|
|
raise _Aborted(code, detail)
|
|
|
|
|
|
def _capabilities() -> list[dict]:
|
|
return [
|
|
{
|
|
"engine": ENGINE,
|
|
"model_id": MODEL,
|
|
"operations": [OP],
|
|
"supported": True,
|
|
"installed": True,
|
|
"downloaded": True,
|
|
"resident": False,
|
|
"backend": "cuda",
|
|
"free_memory_bytes": 24 * 1024**3,
|
|
}
|
|
]
|
|
|
|
|
|
class _Plane:
|
|
"""A servicer with one enrolled worker, driven RPC by RPC."""
|
|
|
|
def __init__(self, tmp_path) -> None:
|
|
self.artifact_dir = str(tmp_path / "artifacts")
|
|
self.pool = WorkerPool()
|
|
self.scheduler = Scheduler(self.pool)
|
|
self.servicer = WorkerServicer(
|
|
self.scheduler, self.pool, artifact_dir=self.artifact_dir
|
|
)
|
|
self.keypair = WorkerKeypair.generate()
|
|
self.worker_id = ""
|
|
self.epoch = 0
|
|
|
|
async def register(self) -> None:
|
|
token = registry.create_enrollment(endpoint="localhost:1", cert_fingerprint="fp").encode()
|
|
challenge, nonce = identity.new_challenge(), identity.new_challenge()
|
|
signature = self.keypair.sign(
|
|
identity.challenge_message(
|
|
challenge=challenge,
|
|
worker_id=self.worker_id,
|
|
session_epoch=self.epoch,
|
|
nonce=nonce,
|
|
)
|
|
)
|
|
response = await self.servicer.Register(
|
|
pb.RegisterRequest(
|
|
features=sorted(REQUIRED_FEATURES),
|
|
envelope=pb.Envelope(sequence=self.epoch),
|
|
protocol_version_min=1,
|
|
protocol_version_max=1,
|
|
enrollment_token=token,
|
|
public_key=self.keypair.public_bytes(),
|
|
challenge=challenge,
|
|
challenge_signature=signature,
|
|
nonce=nonce,
|
|
key_id=self.keypair.key_id,
|
|
host=codec.host_to_pb({"hostname": "gpu2", "os": "linux", "arch": "x86_64"}),
|
|
capabilities=[codec.capability_to_pb(c) for c in _capabilities()],
|
|
max_concurrent_tasks=2,
|
|
),
|
|
_Context(),
|
|
)
|
|
assert not response.error.code, response.error.code
|
|
self.worker_id = response.worker_id
|
|
self.epoch = response.session_epoch
|
|
|
|
@property
|
|
def token(self) -> str:
|
|
return self.servicer._sessions[self.worker_id].session.token
|
|
|
|
def running(self):
|
|
"""One task assigned to this worker and rendering."""
|
|
task = self.scheduler.submit(operation=OP, engine=ENGINE, model_id=MODEL)
|
|
assignment = self.scheduler.next_assignment()
|
|
assert assignment is not None
|
|
attempt = assignment.attempt
|
|
self.scheduler.on_accepted(task.task_id, attempt.attempt_id, epoch=attempt.session_epoch)
|
|
self.scheduler.on_started(task.task_id, attempt.attempt_id, epoch=attempt.session_epoch)
|
|
return task, attempt
|
|
|
|
def final_path(self, task, attempt) -> str:
|
|
return os.path.join(self.artifact_dir, task.task_id, f"{attempt.attempt_id}.bin")
|
|
|
|
async def upload(self, chunks) -> pb.ResultAck:
|
|
return await self.servicer.UploadResult(_aiter(chunks), _Context(self.token))
|
|
|
|
async def download(self, ref, *, context=None):
|
|
collected = []
|
|
async for chunk in self.servicer.DownloadArtifact(ref, context or _Context(self.token)):
|
|
collected.append(chunk)
|
|
return collected
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def plane(tmp_path, db):
|
|
p = _Plane(tmp_path)
|
|
await p.register()
|
|
return p
|
|
|
|
|
|
async def _aiter(items):
|
|
for item in items:
|
|
yield item
|
|
|
|
|
|
def _ref(plane, task, attempt, *, payload=b"", sha256=None, size=None) -> pb.ArtifactRef:
|
|
return pb.ArtifactRef(
|
|
artifact_id="",
|
|
task_id=task.task_id,
|
|
attempt_id=attempt.attempt_id,
|
|
filename="result.wav",
|
|
size_bytes=len(payload) if size is None else size,
|
|
sha256=hashlib.sha256(payload).hexdigest() if sha256 is None else sha256,
|
|
session_token=plane.token,
|
|
)
|
|
|
|
|
|
def _chunks(ref, payload: bytes, *, size: int = 4, last: bool = True):
|
|
"""Split ``payload`` into offset-correct chunks."""
|
|
out = []
|
|
for start in range(0, len(payload), size):
|
|
out.append(pb.ResultChunk(ref=ref, offset=start, data=payload[start : start + size]))
|
|
if out and last:
|
|
out[-1].last = True
|
|
return out
|
|
|
|
|
|
# ── Commit only against a verified, complete transfer ──────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_verified_upload_commits_under_its_own_attempt(plane):
|
|
task, attempt = plane.running()
|
|
payload = b"rendered audio bytes"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
|
|
ack = await plane.upload(_chunks(ref, payload))
|
|
|
|
assert ack.committed is True
|
|
assert ack.bytes_received == len(payload)
|
|
final = plane.final_path(task, attempt)
|
|
assert open(final, "rb").read() == payload
|
|
assert not os.path.exists(f"{final}.part")
|
|
# The id handed back is store-relative, and re-resolves to what was
|
|
# written — the worker never learns our filesystem layout.
|
|
assert not os.path.isabs(ack.artifact_id)
|
|
assert plane.servicer._contained_artifact(ack.artifact_id) == final
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verified_upload_atomically_replaces_an_existing_attempt_file(plane):
|
|
task, attempt = plane.running()
|
|
final = plane.final_path(task, attempt)
|
|
os.makedirs(os.path.dirname(final), exist_ok=True)
|
|
with open(final, "wb") as fh:
|
|
fh.write(b"stale result")
|
|
payload = b"new verified result"
|
|
|
|
ack = await plane.upload(_chunks(_ref(plane, task, attempt, payload=payload), payload))
|
|
|
|
assert ack.committed is True
|
|
assert open(final, "rb").read() == payload
|
|
|
|
|
|
@pytest.mark.parametrize("task_id", ["CON", "NUL.txt", "name.", "x" * 241])
|
|
@pytest.mark.asyncio
|
|
async def test_windows_hostile_artifact_components_are_refused(plane, task_id):
|
|
assert plane.servicer._artifact_path(task_id, "attempt") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_stream_that_ends_without_a_last_chunk_commits_nothing(plane):
|
|
"""The iterator simply stopping is a truncated transfer, not a result.
|
|
|
|
This committed whatever had arrived, renamed it into place, and returned
|
|
committed=True — so a dropped connection two thirds of the way through a
|
|
render delivered two thirds of a render as the finished article.
|
|
"""
|
|
task, attempt = plane.running()
|
|
payload = b"half a render, and then the link died"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
|
|
ack = await plane.upload(_chunks(ref, payload[:12], last=False))
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "UPLOAD_INCOMPLETE"
|
|
assert ack.bytes_received == 12
|
|
final = plane.final_path(task, attempt)
|
|
assert not os.path.exists(final)
|
|
# Kept, so the resume has something to resume onto.
|
|
assert os.path.getsize(f"{final}.part") == 12
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_digest_mismatch_is_never_renamed_into_place(plane):
|
|
task, attempt = plane.running()
|
|
payload = b"corrupted on the wire"
|
|
# Same length, different bytes: only the digest can tell these apart.
|
|
ref = _ref(plane, task, attempt, payload=b"what the worker sent!")
|
|
|
|
ack = await plane.upload(_chunks(ref, payload))
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "DIGEST_MISMATCH"
|
|
final = plane.final_path(task, attempt)
|
|
assert not os.path.exists(final)
|
|
# And the bad bytes are gone: a resume must not append onto them.
|
|
assert not os.path.exists(f"{final}.part")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_upload_with_no_declared_digest_is_refused_before_any_bytes(plane):
|
|
task, attempt = plane.running()
|
|
payload = b"unverifiable"
|
|
ref = _ref(plane, task, attempt, payload=payload, sha256="")
|
|
|
|
ack = await plane.upload(_chunks(ref, payload))
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "DIGEST_REQUIRED"
|
|
assert not os.path.exists(f"{plane.final_path(task, attempt)}.part")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_size_that_disagrees_with_the_bytes_delivered_is_refused(plane):
|
|
task, attempt = plane.running()
|
|
payload = b"eight..."
|
|
ref = _ref(plane, task, attempt, payload=payload, size=len(payload) + 4)
|
|
|
|
ack = await plane.upload(_chunks(ref, payload))
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "SIZE_MISMATCH"
|
|
assert not os.path.exists(plane.final_path(task, attempt))
|
|
|
|
|
|
# ── Offsets are checked, and the ack is the bytes-held probe ───────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_chunk_at_the_wrong_offset_is_refused_with_the_bytes_held(plane):
|
|
"""``chunk.offset`` was read as a truthiness flag and then ignored, so a
|
|
gap or an overlap was appended as though it were the next byte."""
|
|
task, attempt = plane.running()
|
|
payload = b"0123456789abcdef"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
stream = [
|
|
pb.ResultChunk(ref=ref, offset=0, data=payload[:4]),
|
|
pb.ResultChunk(ref=ref, offset=999, data=payload[4:], last=True),
|
|
]
|
|
|
|
ack = await plane.upload(stream)
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "OFFSET_MISMATCH"
|
|
# The only report of "bytes already held" this RPC can make: one terminal
|
|
# ack, carrying the offset to resume from.
|
|
assert ack.bytes_received == 4
|
|
assert not os.path.exists(plane.final_path(task, attempt))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_resume_hashes_the_bytes_already_on_disk(plane):
|
|
"""Otherwise the digest would attest only to the resumed tail — verifying
|
|
the half of the file that was never in doubt."""
|
|
task, attempt = plane.running()
|
|
payload = b"the first half of it | and the second half of it"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
|
|
dropped = await plane.upload(_chunks(ref, payload[:20], last=False))
|
|
assert dropped.bytes_received == 20
|
|
|
|
resumed = await plane.upload(
|
|
[pb.ResultChunk(ref=ref, offset=20, data=payload[20:], last=True)]
|
|
)
|
|
|
|
assert resumed.committed is True
|
|
assert resumed.bytes_received == len(payload)
|
|
assert open(plane.final_path(task, attempt), "rb").read() == payload
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_resume_onto_corrupted_held_bytes_still_fails_verification(plane):
|
|
task, attempt = plane.running()
|
|
payload = b"the first half of it | and the second half of it"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
await plane.upload(_chunks(ref, b"tampered with here!!", last=False))
|
|
|
|
resumed = await plane.upload(
|
|
[pb.ResultChunk(ref=ref, offset=20, data=payload[20:], last=True)]
|
|
)
|
|
|
|
assert resumed.committed is False
|
|
assert resumed.error.code == "DIGEST_MISMATCH"
|
|
assert not os.path.exists(plane.final_path(task, attempt))
|
|
|
|
|
|
# ── Ceilings ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_upload_past_its_declared_size_is_cut_off(plane):
|
|
"""A declared size narrows the cap; it cannot be exceeded by streaming."""
|
|
task, attempt = plane.running()
|
|
ref = _ref(plane, task, attempt, payload=b"tiny", size=4)
|
|
|
|
ack = await plane.upload(_chunks(ref, b"very much larger than four bytes"))
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "ARTIFACT_TOO_LARGE"
|
|
final = plane.final_path(task, attempt)
|
|
assert not os.path.exists(final)
|
|
assert not os.path.exists(f"{final}.part")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_undeclared_upload_is_bounded_by_the_artifact_ceiling(plane, monkeypatch):
|
|
monkeypatch.setattr(server_module, "MAX_ARTIFACT_BYTES", 8)
|
|
task, attempt = plane.running()
|
|
ref = _ref(plane, task, attempt, payload=b"sixteen bytes!!!", size=0)
|
|
|
|
ack = await plane.upload(_chunks(ref, b"sixteen bytes!!!"))
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "ARTIFACT_TOO_LARGE"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_per_task_artifact_budget_is_enforced_across_attempts(plane, monkeypatch):
|
|
"""One artifact under the cap, twice, must not add up to more than a task
|
|
is allowed to deliver."""
|
|
monkeypatch.setattr(server_module, "MAX_TASK_ARTIFACT_BYTES", 24)
|
|
task, attempt = plane.running()
|
|
payload = b"sixteen bytes!!!"
|
|
first = await plane.upload(_chunks(ref := _ref(plane, task, attempt, payload=payload), payload))
|
|
assert first.committed is True
|
|
assert ref.size_bytes == 16
|
|
|
|
# A second attempt of the same task, delivering another 16 bytes. CAPACITY
|
|
# so the retry can land on the same worker — anything else excludes it.
|
|
plane.scheduler.on_failed(
|
|
task.task_id,
|
|
attempt.attempt_id,
|
|
WorkerError(error_class=ErrorClass.CAPACITY, code="RETRY", message="again"),
|
|
epoch=attempt.session_epoch,
|
|
)
|
|
retry = plane.scheduler.next_assignment()
|
|
assert retry is not None
|
|
plane.scheduler.on_accepted(task.task_id, retry.attempt.attempt_id, epoch=retry.attempt.session_epoch)
|
|
plane.scheduler.on_started(task.task_id, retry.attempt.attempt_id, epoch=retry.attempt.session_epoch)
|
|
|
|
ack = await plane.upload(
|
|
_chunks(_ref(plane, task, retry.attempt, payload=payload), payload)
|
|
)
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "TASK_BUDGET_EXCEEDED"
|
|
|
|
|
|
# ── The delivery phase actually exists ─────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_upload_moves_the_task_into_result_uploading(plane):
|
|
"""``Task.uploading`` had zero callers, so RESULT_UPLOADING was
|
|
unreachable and every byte of delivery ran under the execution phase."""
|
|
task, attempt = plane.running()
|
|
payload = b"0123456789abcdef"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
seen: list[TaskState] = []
|
|
|
|
async def observed():
|
|
for chunk in _chunks(ref, payload):
|
|
yield chunk
|
|
seen.append(task.state)
|
|
|
|
ack = await plane.servicer.UploadResult(observed(), _Context(plane.token))
|
|
|
|
assert ack.committed is True
|
|
assert seen[0] is TaskState.RESULT_UPLOADING
|
|
assert attempt.state is AttemptState.UPLOADING
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_upload_lease_runs_on_the_result_delivery_budget(plane):
|
|
"""A slow delivery is bounded by ``result_delivery_seconds`` (900s), not by
|
|
the execution budget it used to inherit — which is what made a large
|
|
upload die mid-transfer under the 120s progress lease."""
|
|
task, attempt = plane.running()
|
|
budget = deadline_policy.for_task(OP)
|
|
payload = b"0123456789abcdef"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
|
|
async def slow():
|
|
chunks = _chunks(ref, payload)
|
|
yield chunks[0]
|
|
# Age the delivery phase past the execution budget but well inside the
|
|
# delivery one. Under the old code there was no delivery phase, so the
|
|
# keepalive ceiling clamped the lease into the past and the next sweep
|
|
# would have failed a task that was uploading fine.
|
|
attempt.phase_started_at = resolve(None) - (budget.execution_seconds + 60)
|
|
for chunk in chunks[1:]:
|
|
yield chunk
|
|
|
|
ack = await plane.servicer.UploadResult(slow(), _Context(plane.token))
|
|
|
|
assert ack.committed is True
|
|
assert budget.result_delivery_seconds > budget.execution_seconds + 60
|
|
assert not attempt.lease_expired()
|
|
|
|
|
|
def test_upload_keepalive_does_not_move_completed_progress_backwards(plane):
|
|
task, attempt = plane.running()
|
|
plane.scheduler.on_progress(
|
|
task.task_id, attempt.attempt_id, progress=1.0, epoch=attempt.session_epoch
|
|
)
|
|
|
|
plane.servicer._renew_upload_lease(attempt)
|
|
|
|
assert attempt.progress == 1.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_upload_onto_a_cancelled_task_is_refused(plane):
|
|
task, attempt = plane.running()
|
|
payload = b"too late"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
plane.scheduler.cancel(task.task_id)
|
|
|
|
ack = await plane.upload(_chunks(ref, payload))
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "ATTEMPT_NOT_LIVE"
|
|
assert not os.path.exists(plane.final_path(task, attempt))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_upload_for_another_workers_attempt_is_refused(plane):
|
|
task, attempt = plane.running()
|
|
payload = b"not yours"
|
|
ref = _ref(plane, task, attempt, payload=payload)
|
|
plane.servicer._sessions[plane.worker_id].worker_id = "someone-else"
|
|
|
|
ack = await plane.upload(_chunks(ref, payload))
|
|
|
|
assert ack.committed is False
|
|
assert ack.error.code == "UNKNOWN_ATTEMPT"
|
|
assert not os.path.exists(plane.final_path(task, attempt))
|
|
|
|
|
|
# ── Serving staged inputs ──────────────────────────────────────────────────
|
|
|
|
|
|
def _stage(plane, name: str, data: bytes) -> str:
|
|
"""Stand in for the input-staging step: a file inside the artifact store."""
|
|
path = os.path.join(plane.artifact_dir, "inputs", name)
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "wb") as fh:
|
|
fh.write(data)
|
|
return os.path.join("inputs", name)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_worker_can_read_the_input_staged_for_its_own_task(plane):
|
|
task, attempt = plane.running()
|
|
artifact_id = _stage(plane, "voice.wav", b"reference audio" * 10)
|
|
|
|
chunks = await plane.download(
|
|
pb.ArtifactRef(
|
|
artifact_id=artifact_id,
|
|
task_id=task.task_id,
|
|
attempt_id=attempt.attempt_id,
|
|
session_token=plane.token,
|
|
)
|
|
)
|
|
|
|
assert b"".join(c.data for c in chunks) == b"reference audio" * 10
|
|
assert chunks[-1].last is True
|
|
assert chunks[0].ref.size_bytes == len(b"reference audio" * 10)
|
|
# Nothing hands a session token back out that did not have to go out.
|
|
assert all(not c.ref.session_token for c in chunks)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dispatch_stages_and_serves_from_the_servicers_artifact_root(plane, tmp_path):
|
|
voice = tmp_path / "voice.wav"
|
|
voice.write_bytes(b"reference audio")
|
|
task = plane.scheduler.submit(
|
|
operation=OP, engine=ENGINE, model_id=MODEL, params={"ref_audio": str(voice)}
|
|
)
|
|
assignment = plane.scheduler.next_assignment()
|
|
assert assignment is not None
|
|
|
|
assert await plane.servicer.dispatch(assignment)
|
|
message = await plane.servicer._sessions[plane.worker_id].outbox.get()
|
|
wire = message.assignment
|
|
assert wire.inputs
|
|
staged = os.path.join(plane.artifact_dir, wire.inputs[0].artifact_id)
|
|
assert os.path.isfile(staged)
|
|
|
|
chunks = await plane.download(
|
|
pb.ArtifactRef(
|
|
artifact_id=wire.inputs[0].artifact_id,
|
|
task_id=task.task_id,
|
|
attempt_id=assignment.attempt.attempt_id,
|
|
session_token=plane.token,
|
|
)
|
|
)
|
|
assert b"".join(chunk.data for chunk in chunks) == voice.read_bytes()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_worker_cannot_read_an_input_for_a_task_it_is_not_running(plane):
|
|
"""Authentication is not authorisation: from this phase on, staged inputs
|
|
are the user's own reference audio."""
|
|
task, attempt = plane.running()
|
|
artifact_id = _stage(plane, "voice.wav", b"reference audio")
|
|
plane.servicer._sessions[plane.worker_id].worker_id = "someone-else"
|
|
|
|
with pytest.raises(_Aborted) as caught:
|
|
await plane.download(
|
|
pb.ArtifactRef(
|
|
artifact_id=artifact_id,
|
|
task_id=task.task_id,
|
|
attempt_id=attempt.attempt_id,
|
|
session_token=plane.token,
|
|
)
|
|
)
|
|
|
|
assert "PERMISSION_DENIED" in str(caught.value.code)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_input_request_naming_no_task_is_refused(plane):
|
|
plane.running()
|
|
artifact_id = _stage(plane, "voice.wav", b"reference audio")
|
|
|
|
with pytest.raises(_Aborted) as caught:
|
|
await plane.download(
|
|
pb.ArtifactRef(artifact_id=artifact_id, session_token=plane.token)
|
|
)
|
|
|
|
assert "PERMISSION_DENIED" in str(caught.value.code)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_input_outside_the_artifact_store_is_not_served(plane, tmp_path):
|
|
task, attempt = plane.running()
|
|
secret = tmp_path / "secret.txt"
|
|
secret.write_text("private")
|
|
|
|
for artifact_id in ("../secret.txt", str(secret), "/etc/passwd"):
|
|
with pytest.raises(_Aborted) as caught:
|
|
await plane.download(
|
|
pb.ArtifactRef(
|
|
artifact_id=artifact_id,
|
|
task_id=task.task_id,
|
|
attempt_id=attempt.attempt_id,
|
|
session_token=plane.token,
|
|
)
|
|
)
|
|
assert "NOT_FOUND" in str(caught.value.code)
|