feat(observatory): add portable LAB V1 foundation
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.observatory.portable_queue_binding import (
|
||||
PortableQueueBindingIntegrityError,
|
||||
PortableQueueBindingStaleCheckError,
|
||||
PortableRecordedQueueBindingService,
|
||||
PortableRecordedRunPreparation,
|
||||
)
|
||||
from k1link.observatory.portable_run_definitions import (
|
||||
PortableExecutorAvailability,
|
||||
PortableRunDefinitionRegistry,
|
||||
PortableRunDefinitionUnavailableError,
|
||||
canonical_sha256,
|
||||
)
|
||||
from k1link.observatory.recorded_jobs import ObservatoryRecordedJobQueue
|
||||
from k1link.observatory.source_admission import (
|
||||
PORTABLE_SOURCE_DOCUMENT_DIRECTORY,
|
||||
PortableRecordedSourceAdmission,
|
||||
PortableSourceNotPreparedError,
|
||||
RecordedK1SourceAdmissionService,
|
||||
)
|
||||
from k1link.sessions.media import (
|
||||
CAMERA_ARCHIVE_SCHEMA,
|
||||
RecordedMediaEpoch,
|
||||
RecordedMediaManifest,
|
||||
RecordedMediaSegment,
|
||||
)
|
||||
from k1link.sessions.models import (
|
||||
RecordedMediaArtifact,
|
||||
ReplayArtifact,
|
||||
ReplayCommand,
|
||||
SessionArtifact,
|
||||
SessionDetail,
|
||||
SessionSource,
|
||||
SessionSummary,
|
||||
)
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json"
|
||||
SESSION_ID = "20260831T000000Z_viewer_live"
|
||||
CATALOG_SHA256 = "1" * 64
|
||||
RAW_SHA256 = "2" * 64
|
||||
GENERATION_SHA256 = "3" * 64
|
||||
SEGMENT_SHA256 = "4" * 64
|
||||
INIT_SHA256 = "e2279963e16d84c91d68e7dbb1f7efed840533387dfeb844b7398bff45fbde38"
|
||||
|
||||
|
||||
def _blocked_registry() -> PortableRunDefinitionRegistry:
|
||||
return PortableRunDefinitionRegistry.from_file(REGISTRY_PATH)
|
||||
|
||||
|
||||
def _ready_registry() -> PortableRunDefinitionRegistry:
|
||||
blocked = _blocked_registry().definitions[0]
|
||||
executor = PortableExecutorAvailability(
|
||||
contour_id="worker-006",
|
||||
state="ready",
|
||||
release_id="lab-v1-eomt-ddrnet-executor-v1",
|
||||
release_sha256="5" * 64,
|
||||
image_sha256="6" * 64,
|
||||
reason_code=None,
|
||||
reason=None,
|
||||
)
|
||||
identity = blocked.identity_document()
|
||||
identity["executor"] = executor.identity_document()
|
||||
ready = replace(
|
||||
blocked,
|
||||
executor=executor,
|
||||
definition_sha256=canonical_sha256(identity),
|
||||
)
|
||||
return PortableRunDefinitionRegistry((ready,))
|
||||
|
||||
|
||||
def _detail(*, display_name: str = "arbitrary-operator-label") -> SessionDetail:
|
||||
return SessionDetail(
|
||||
summary=SessionSummary(
|
||||
session_id=SESSION_ID,
|
||||
display_name=display_name,
|
||||
status="ready",
|
||||
started_at_utc="2026-08-31T00:00:00Z",
|
||||
completed_at_utc="2026-08-31T00:10:00Z",
|
||||
duration_seconds=600.0,
|
||||
modalities=("point-cloud", "trajectory", "video"),
|
||||
source_count=3,
|
||||
total_bytes=120,
|
||||
replayable=True,
|
||||
origin="xgrids-k1.viewer-live.evidence",
|
||||
),
|
||||
sources=(
|
||||
SessionSource(
|
||||
source_id="sensor.camera.right",
|
||||
semantic_channel_id="camera.video.recorded",
|
||||
modality="video",
|
||||
status="recorded",
|
||||
seekable=True,
|
||||
artifact_id="recorded-video-right",
|
||||
),
|
||||
SessionSource(
|
||||
source_id="sensor.lidar.primary",
|
||||
semantic_channel_id="spatial.point-cloud.recorded",
|
||||
modality="point-cloud",
|
||||
status="recorded",
|
||||
seekable=True,
|
||||
artifact_id="raw-transport-primary",
|
||||
),
|
||||
SessionSource(
|
||||
source_id="spatial.trajectory",
|
||||
semantic_channel_id="spatial.pose.recorded",
|
||||
modality="trajectory",
|
||||
status="recorded",
|
||||
seekable=True,
|
||||
artifact_id="raw-transport-primary",
|
||||
),
|
||||
),
|
||||
artifacts=(
|
||||
SessionArtifact(
|
||||
artifact_id="raw-transport-primary",
|
||||
kind="raw-transport",
|
||||
media_type="application/x-nodedc-k1mqtt",
|
||||
byte_length=100,
|
||||
sha256=RAW_SHA256,
|
||||
integrity_status="verified",
|
||||
),
|
||||
SessionArtifact(
|
||||
artifact_id="recorded-video-right",
|
||||
kind="recorded-video",
|
||||
media_type="video/mp4",
|
||||
byte_length=20,
|
||||
sha256=None,
|
||||
integrity_status="validated-structure",
|
||||
),
|
||||
),
|
||||
plugin_id="nodedc.device.xgrids-lixelkity-k1",
|
||||
archive_id="xgrids-k1.viewer-live.evidence",
|
||||
)
|
||||
|
||||
|
||||
def _replay(root: Path) -> ReplayCommand:
|
||||
return ReplayCommand(
|
||||
session_id=SESSION_ID,
|
||||
plugin_id="nodedc.device.xgrids-lixelkity-k1",
|
||||
allowed_root=root,
|
||||
session_root=root / SESSION_ID,
|
||||
primary_artifact_id="raw-transport-primary",
|
||||
artifacts=(
|
||||
ReplayArtifact(
|
||||
artifact_id="raw-transport-primary",
|
||||
path=root / "mqtt.raw.k1mqtt",
|
||||
media_type="application/x-nodedc-k1mqtt",
|
||||
file_byte_length=100,
|
||||
replay_byte_length=100,
|
||||
expected_sha256=RAW_SHA256,
|
||||
),
|
||||
),
|
||||
timeline_origin_epoch_ns=1,
|
||||
timeline_origin_monotonic_ns=2,
|
||||
speed=1.0,
|
||||
loop=False,
|
||||
)
|
||||
|
||||
|
||||
def _media_artifact(root: Path) -> RecordedMediaArtifact:
|
||||
return RecordedMediaArtifact(
|
||||
session_id=SESSION_ID,
|
||||
public_source_id="recorded.camera.right",
|
||||
artifact_id="recorded-video-right",
|
||||
source_path=root / "media" / "sensor.camera.right",
|
||||
byte_length=20,
|
||||
)
|
||||
|
||||
|
||||
def _manifest(root: Path) -> RecordedMediaManifest:
|
||||
epoch = RecordedMediaEpoch(
|
||||
ordinal=1,
|
||||
path=root / "epoch-1",
|
||||
init_path=root / "epoch-1" / "init.mp4",
|
||||
init_byte_length=10,
|
||||
init_sha256=INIT_SHA256,
|
||||
media_type='video/mp4; codecs="avc1.641028"',
|
||||
timeline_start_seconds=10.0,
|
||||
timeline_end_seconds=10.1,
|
||||
segments=(
|
||||
RecordedMediaSegment(
|
||||
sequence=1,
|
||||
path=root / "epoch-1" / "segments" / "1.m4s",
|
||||
byte_length=10,
|
||||
sha256=SEGMENT_SHA256,
|
||||
random_access=True,
|
||||
end_time_seconds=10.1,
|
||||
),
|
||||
),
|
||||
)
|
||||
return RecordedMediaManifest(
|
||||
session_id=SESSION_ID,
|
||||
public_source_id="recorded.camera.right",
|
||||
artifact_id="recorded-video-right",
|
||||
synchronization="host-arrival-best-effort",
|
||||
generation_sha256=GENERATION_SHA256,
|
||||
timeline_start_seconds=10.0,
|
||||
timeline_end_seconds=10.1,
|
||||
byte_length=20,
|
||||
epochs=(epoch,),
|
||||
)
|
||||
|
||||
|
||||
class _Store:
|
||||
def __init__(
|
||||
self,
|
||||
root: Path,
|
||||
*,
|
||||
catalogs: tuple[str, ...] = (CATALOG_SHA256,),
|
||||
) -> None:
|
||||
self.data_dir = root.resolve()
|
||||
self.detail = _detail()
|
||||
self.replay = _replay(root)
|
||||
self.media = (_media_artifact(root),)
|
||||
self.catalogs = catalogs
|
||||
self.catalog_reads = 0
|
||||
self.prepare_replay_calls = 0
|
||||
self.recorded_media_reads = 0
|
||||
|
||||
def get_session_with_catalog_snapshot(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> tuple[SessionDetail, str]:
|
||||
assert session_id == SESSION_ID
|
||||
index = min(self.catalog_reads, len(self.catalogs) - 1)
|
||||
self.catalog_reads += 1
|
||||
return self.detail, self.catalogs[index]
|
||||
|
||||
def prepare_replay(self, session_id: str) -> ReplayCommand:
|
||||
assert session_id == SESSION_ID
|
||||
self.prepare_replay_calls += 1
|
||||
return self.replay
|
||||
|
||||
def list_recorded_media(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> tuple[RecordedMediaArtifact, ...]:
|
||||
assert session_id == SESSION_ID
|
||||
self.recorded_media_reads += 1
|
||||
return self.media
|
||||
|
||||
|
||||
class _Inspector:
|
||||
def __init__(self, manifest: RecordedMediaManifest | None) -> None:
|
||||
self.manifest = manifest
|
||||
self.inspect_calls = 0
|
||||
self.restore_calls = 0
|
||||
|
||||
def inspect(
|
||||
self,
|
||||
artifact: RecordedMediaArtifact,
|
||||
replay: ReplayCommand,
|
||||
) -> RecordedMediaManifest:
|
||||
assert artifact.session_id == replay.session_id
|
||||
self.inspect_calls += 1
|
||||
assert self.manifest is not None
|
||||
return self.manifest
|
||||
|
||||
def restore_prepared(
|
||||
self,
|
||||
artifact: RecordedMediaArtifact,
|
||||
replay: ReplayCommand,
|
||||
) -> RecordedMediaManifest | None:
|
||||
assert artifact.session_id == replay.session_id
|
||||
self.restore_calls += 1
|
||||
return self.manifest
|
||||
|
||||
|
||||
def _write_probe_summary(root: Path, *, segment_count: int = 11) -> None:
|
||||
epoch = root / "media" / "sensor.camera.right" / "epoch-1"
|
||||
epoch.mkdir(parents=True)
|
||||
(epoch / "summary.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": CAMERA_ARCHIVE_SCHEMA,
|
||||
"source_id": "sensor.camera.right",
|
||||
"codec_epoch": 1,
|
||||
"status": "complete",
|
||||
"segment_count": segment_count,
|
||||
"entry_count": segment_count,
|
||||
"media_segment_count": segment_count,
|
||||
"init_sha256": INIT_SHA256,
|
||||
"synchronization": "host-arrival-best-effort",
|
||||
"commit_policy": "per-segment-fsync",
|
||||
"failure_code": None,
|
||||
"artifacts": {
|
||||
"init": "init.mp4",
|
||||
"segments": "segments",
|
||||
"index": "index.jsonl",
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _service(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
registry: PortableRunDefinitionRegistry,
|
||||
store: _Store | None = None,
|
||||
inspector: _Inspector | None = None,
|
||||
with_queue: bool = True,
|
||||
) -> tuple[
|
||||
PortableRecordedQueueBindingService,
|
||||
_Store,
|
||||
ObservatoryRecordedJobQueue | None,
|
||||
_Inspector,
|
||||
]:
|
||||
active_store = store or _Store(tmp_path)
|
||||
queue = None
|
||||
if with_queue:
|
||||
queue = ObservatoryRecordedJobQueue(
|
||||
tmp_path,
|
||||
definitions=registry.to_recorded_registry(),
|
||||
clock=lambda: "2026-08-31T00:00:00.000Z",
|
||||
)
|
||||
active_inspector = inspector or _Inspector(_manifest(tmp_path))
|
||||
service = PortableRecordedQueueBindingService(
|
||||
data_dir=tmp_path,
|
||||
session_store=active_store, # type: ignore[arg-type]
|
||||
media_inspector=active_inspector, # type: ignore[arg-type]
|
||||
definitions=registry,
|
||||
queue=queue,
|
||||
)
|
||||
return service, active_store, queue, active_inspector
|
||||
|
||||
|
||||
def _check(
|
||||
service: PortableRecordedQueueBindingService,
|
||||
registry: PortableRunDefinitionRegistry,
|
||||
) -> PortableRecordedRunPreparation:
|
||||
definition = registry.definitions[0]
|
||||
return service.check(
|
||||
source_session_id=SESSION_ID,
|
||||
setup_id=definition.setup_id,
|
||||
definition_sha256=definition.definition_sha256,
|
||||
)
|
||||
|
||||
|
||||
def test_not_installed_definition_fails_before_source_or_queue_writes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = _blocked_registry()
|
||||
service, store, queue, _inspector = _service(
|
||||
tmp_path,
|
||||
registry=registry,
|
||||
with_queue=False,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
PortableRunDefinitionUnavailableError,
|
||||
match="not sealed or installed",
|
||||
):
|
||||
_check(service, registry)
|
||||
|
||||
assert store.catalog_reads == 0
|
||||
assert queue is None
|
||||
assert not (tmp_path / PORTABLE_SOURCE_DOCUMENT_DIRECTORY).exists()
|
||||
assert not (tmp_path / "observatory-recorded-jobs.sqlite3").exists()
|
||||
|
||||
|
||||
def test_probe_is_bounded_and_does_not_enter_replay_or_media_inspector(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = _ready_registry()
|
||||
_write_probe_summary(tmp_path, segment_count=31)
|
||||
service, store, queue, inspector = _service(tmp_path, registry=registry)
|
||||
definition = registry.definitions[0]
|
||||
|
||||
capability = service.probe(
|
||||
source_session_id=SESSION_ID,
|
||||
setup_id=definition.setup_id,
|
||||
definition_sha256=definition.definition_sha256,
|
||||
)
|
||||
|
||||
assert capability.source_session_id == SESSION_ID
|
||||
assert capability.source_catalog_sha256 == CATALOG_SHA256
|
||||
assert capability.source_adapter_sha256 == definition.source_adapter.contract_sha256
|
||||
assert capability.camera_segment_count == 31
|
||||
assert store.catalog_reads == 1
|
||||
assert store.recorded_media_reads == 1
|
||||
assert store.prepare_replay_calls == 0
|
||||
assert inspector.inspect_calls == 0
|
||||
assert inspector.restore_calls == 0
|
||||
assert not (tmp_path / "media" / "sensor.camera.right" / "epoch-1" / "segments").exists()
|
||||
assert not (tmp_path / PORTABLE_SOURCE_DOCUMENT_DIRECTORY).exists()
|
||||
assert queue is not None
|
||||
assert queue.list_jobs() == ()
|
||||
|
||||
|
||||
def test_check_fails_typed_when_media_was_never_prepared(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = _ready_registry()
|
||||
empty_inspector = _Inspector(None)
|
||||
service, _, queue, inspector = _service(
|
||||
tmp_path,
|
||||
registry=registry,
|
||||
inspector=empty_inspector,
|
||||
)
|
||||
|
||||
with pytest.raises(PortableSourceNotPreparedError, match="not been prepared"):
|
||||
_check(service, registry)
|
||||
|
||||
assert inspector.inspect_calls == 0
|
||||
assert inspector.restore_calls == 1
|
||||
assert not (tmp_path / PORTABLE_SOURCE_DOCUMENT_DIRECTORY).exists()
|
||||
assert queue is not None
|
||||
assert queue.list_jobs() == ()
|
||||
|
||||
|
||||
def test_check_is_path_free_read_only_and_builds_exact_queue_intent(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = _ready_registry()
|
||||
service, _, queue, inspector = _service(tmp_path, registry=registry)
|
||||
|
||||
checked = _check(service, registry)
|
||||
intent = checked.intent(idempotency_key="portable-run-001")
|
||||
|
||||
assert intent.source_session_id == SESSION_ID
|
||||
assert intent.source_catalog_sha256 == checked.source.source_catalog_sha256
|
||||
assert intent.source_bundle_sha256 == checked.source.source_bundle_sha256
|
||||
assert intent.source_capability_manifest_sha256 == (
|
||||
checked.source.source_capability_manifest_sha256
|
||||
)
|
||||
assert intent.setup_id == registry.definitions[0].setup_id
|
||||
assert intent.definition_sha256 == registry.definitions[0].definition_sha256
|
||||
assert checked.source.source_adapter_sha256 == (
|
||||
registry.definitions[0].source_adapter.contract_sha256
|
||||
)
|
||||
assert "path" not in str(checked.as_dict()).lower()
|
||||
assert not (tmp_path / PORTABLE_SOURCE_DOCUMENT_DIRECTORY).exists()
|
||||
assert queue is not None
|
||||
assert queue.list_jobs() == ()
|
||||
assert inspector.inspect_calls == 0
|
||||
assert inspector.restore_calls == 1
|
||||
|
||||
|
||||
def test_admit_persists_exact_documents_and_submit_queues_same_identity(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = _ready_registry()
|
||||
service, _, queue, _inspector = _service(tmp_path, registry=registry)
|
||||
checked = _check(service, registry)
|
||||
definition = registry.definitions[0]
|
||||
|
||||
admitted = service.admit(
|
||||
source_session_id=SESSION_ID,
|
||||
setup_id=definition.setup_id,
|
||||
definition_sha256=definition.definition_sha256,
|
||||
expected_check_sha256=checked.check_sha256,
|
||||
)
|
||||
job, created = service.submit(
|
||||
source_session_id=SESSION_ID,
|
||||
setup_id=definition.setup_id,
|
||||
definition_sha256=definition.definition_sha256,
|
||||
expected_check_sha256=checked.check_sha256,
|
||||
idempotency_key="portable-run-001",
|
||||
)
|
||||
|
||||
root = tmp_path / PORTABLE_SOURCE_DOCUMENT_DIRECTORY
|
||||
assert sorted(path.name for path in root.iterdir()) == sorted(
|
||||
(
|
||||
f"{admitted.source.source_bundle_sha256}.json",
|
||||
f"{admitted.source.source_capability_manifest_sha256}.json",
|
||||
)
|
||||
)
|
||||
assert created is True
|
||||
assert job.state == "queued"
|
||||
assert job.source_catalog_sha256 == admitted.source.source_catalog_sha256
|
||||
assert job.source_bundle_sha256 == admitted.source.source_bundle_sha256
|
||||
assert job.source_capability_manifest_sha256 == (
|
||||
admitted.source.source_capability_manifest_sha256
|
||||
)
|
||||
assert queue is not None
|
||||
assert queue.list_jobs() == (job,)
|
||||
|
||||
|
||||
def test_admit_rejects_catalog_change_since_check_without_writes_or_job(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = _ready_registry()
|
||||
store = _Store(tmp_path, catalogs=(CATALOG_SHA256, "7" * 64))
|
||||
service, _, queue, _inspector = _service(
|
||||
tmp_path,
|
||||
registry=registry,
|
||||
store=store,
|
||||
)
|
||||
checked = _check(service, registry)
|
||||
definition = registry.definitions[0]
|
||||
|
||||
with pytest.raises(PortableQueueBindingStaleCheckError, match="changed"):
|
||||
service.submit(
|
||||
source_session_id=SESSION_ID,
|
||||
setup_id=definition.setup_id,
|
||||
definition_sha256=definition.definition_sha256,
|
||||
expected_check_sha256=checked.check_sha256,
|
||||
idempotency_key="portable-run-stale-001",
|
||||
)
|
||||
|
||||
assert not (tmp_path / PORTABLE_SOURCE_DOCUMENT_DIRECTORY).exists()
|
||||
assert queue is not None
|
||||
assert queue.list_jobs() == ()
|
||||
|
||||
|
||||
def test_admit_rejects_change_during_commit_before_source_documents(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = _ready_registry()
|
||||
store = _Store(
|
||||
tmp_path,
|
||||
catalogs=(
|
||||
CATALOG_SHA256,
|
||||
CATALOG_SHA256,
|
||||
CATALOG_SHA256,
|
||||
"8" * 64,
|
||||
),
|
||||
)
|
||||
service, _, queue, _inspector = _service(
|
||||
tmp_path,
|
||||
registry=registry,
|
||||
store=store,
|
||||
)
|
||||
checked = _check(service, registry)
|
||||
definition = registry.definitions[0]
|
||||
|
||||
with pytest.raises(PortableQueueBindingStaleCheckError, match="changed"):
|
||||
service.submit(
|
||||
source_session_id=SESSION_ID,
|
||||
setup_id=definition.setup_id,
|
||||
definition_sha256=definition.definition_sha256,
|
||||
expected_check_sha256=checked.check_sha256,
|
||||
idempotency_key="portable-run-racing-001",
|
||||
)
|
||||
|
||||
assert not (tmp_path / PORTABLE_SOURCE_DOCUMENT_DIRECTORY).exists()
|
||||
assert queue is not None
|
||||
assert queue.list_jobs() == ()
|
||||
|
||||
|
||||
def test_registry_and_source_admission_adapter_digests_must_match(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
registry = _ready_registry()
|
||||
service, _, queue, _inspector = _service(tmp_path, registry=registry)
|
||||
original_check = RecordedK1SourceAdmissionService.check
|
||||
|
||||
def corrupted_check(
|
||||
source_service: RecordedK1SourceAdmissionService,
|
||||
source_session_id: str,
|
||||
) -> PortableRecordedSourceAdmission:
|
||||
return replace(
|
||||
original_check(source_service, source_session_id),
|
||||
source_adapter_sha256="9" * 64,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(RecordedK1SourceAdmissionService, "check", corrupted_check)
|
||||
|
||||
with pytest.raises(PortableQueueBindingIntegrityError, match="adapter"):
|
||||
_check(service, registry)
|
||||
|
||||
assert not (tmp_path / PORTABLE_SOURCE_DOCUMENT_DIRECTORY).exists()
|
||||
assert queue is not None
|
||||
assert queue.list_jobs() == ()
|
||||
@@ -0,0 +1,322 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from dataclasses import FrozenInstanceError, replace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.observatory.portable_run_definitions import (
|
||||
PORTABLE_MODEL_MANIFEST_SCHEMA,
|
||||
PortableExecutorAvailability,
|
||||
PortableRunDefinitionRegistry,
|
||||
PortableRunDefinitionRegistryError,
|
||||
PortableRunDefinitionUnavailableError,
|
||||
canonical_sha256,
|
||||
)
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json"
|
||||
DEFINITION_SHA256 = "57bf8f0859e10e54e30322c9a8aa28b427699f6fe6b5267e279ec3390fa78466"
|
||||
MODEL_MANIFEST_SHA256 = "3fd2d43af73bd73f89d9ffae95d8770cfdeb46033ec967509124fac6ae4afe56"
|
||||
|
||||
|
||||
def _registry() -> PortableRunDefinitionRegistry:
|
||||
return PortableRunDefinitionRegistry.from_file(REGISTRY_PATH)
|
||||
|
||||
|
||||
def _document() -> dict[str, object]:
|
||||
return json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _write(tmp_path: Path, document: object) -> Path:
|
||||
path = tmp_path / "portable-definitions.json"
|
||||
path.write_text(json.dumps(document), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def _first(document: dict[str, object]) -> dict[str, object]:
|
||||
definitions = document["definitions"]
|
||||
assert isinstance(definitions, list)
|
||||
first = definitions[0]
|
||||
assert isinstance(first, dict)
|
||||
return first
|
||||
|
||||
|
||||
def test_production_definition_is_source_independent_and_requirements_are_immutable() -> None:
|
||||
definition = _registry().definitions[0]
|
||||
requirements = definition.source_requirements
|
||||
|
||||
assert definition.setup_id == "lab-v1-eomt-ddrnet-portable-v1"
|
||||
assert requirements.plugin_id == "nodedc.device.xgrids-lixelkity-k1"
|
||||
assert requirements.archive_id == "xgrids-k1.viewer-live.evidence"
|
||||
assert requirements.required_modalities == ("point-cloud", "trajectory", "video")
|
||||
assert requirements.camera_source_id == "sensor.camera.right"
|
||||
assert requirements.camera_semantic_channel_id == "camera.video.recorded"
|
||||
assert requirements.recorded_media_type == 'video/mp4; codecs="avc1.641028"'
|
||||
assert requirements.recorded_media_init_sha256 == (
|
||||
"e2279963e16d84c91d68e7dbb1f7efed840533387dfeb844b7398bff45fbde38"
|
||||
)
|
||||
assert requirements.camera_width == 800
|
||||
assert requirements.camera_height == 600
|
||||
assert requirements.calibration_slot == "camera_1"
|
||||
assert requirements.calibration_identity_sha256 == (
|
||||
"05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||
)
|
||||
assert requirements.exactly_one_media_epoch is True
|
||||
assert requirements.seekable is True
|
||||
assert not hasattr(requirements, "session_id")
|
||||
assert not hasattr(requirements, "label")
|
||||
|
||||
identity = json.dumps(definition.identity_document(), sort_keys=True)
|
||||
assert "RAVNOVES" not in identity
|
||||
assert "20260828T130511Z_viewer_live" not in identity
|
||||
assert "historical_results" not in identity
|
||||
assert not hasattr(definition, "historical_results")
|
||||
assert "session_id" not in identity
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
requirements.plugin_id = "changed" # type: ignore[misc]
|
||||
|
||||
|
||||
def test_source_requirements_map_exactly_to_admission_contract() -> None:
|
||||
definition = _registry().definitions[0]
|
||||
|
||||
admission = definition.to_source_admission_requirements()
|
||||
|
||||
assert admission.plugin_id == definition.source_requirements.plugin_id
|
||||
assert admission.archive_id == definition.source_requirements.archive_id
|
||||
assert admission.expected_width == definition.source_requirements.camera_width
|
||||
assert admission.expected_height == definition.source_requirements.camera_height
|
||||
assert admission.calibration_slot == definition.source_requirements.calibration_slot
|
||||
assert admission.require_single_camera_epoch is True
|
||||
assert admission.adapter_document() == definition.source_adapter.identity_document(
|
||||
definition.source_requirements
|
||||
)
|
||||
assert admission.adapter_sha256 == definition.source_adapter.contract_sha256
|
||||
|
||||
|
||||
def test_all_canonical_identities_are_recomputed_from_typed_content() -> None:
|
||||
definition = _registry().definitions[0]
|
||||
|
||||
assert definition.definition_sha256 == DEFINITION_SHA256
|
||||
assert canonical_sha256(definition.identity_document()) == DEFINITION_SHA256
|
||||
assert definition.source_adapter.contract_sha256 == canonical_sha256(
|
||||
definition.source_adapter.identity_document(definition.source_requirements)
|
||||
)
|
||||
assert definition.resource_profile.profile_sha256 == canonical_sha256(
|
||||
definition.resource_profile.identity_document()
|
||||
)
|
||||
assert definition.result_contract.contract_sha256 == canonical_sha256(
|
||||
definition.result_contract.identity_document()
|
||||
)
|
||||
assert definition.model_manifest_sha256 == MODEL_MANIFEST_SHA256
|
||||
assert definition.model_manifest_sha256 == canonical_sha256(
|
||||
{
|
||||
"schema_version": PORTABLE_MODEL_MANIFEST_SCHEMA,
|
||||
"models": [model.as_dict() for model in definition.models],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_registry_rejects_legacy_result_metadata(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
document = _document()
|
||||
first = _first(document)
|
||||
first["historical_results"] = []
|
||||
|
||||
with pytest.raises(PortableRunDefinitionRegistryError, match="fields are invalid"):
|
||||
PortableRunDefinitionRegistry.from_file(_write(tmp_path, document))
|
||||
|
||||
|
||||
def test_component_or_model_mutation_without_a_new_digest_is_rejected(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
document = _document()
|
||||
first = _first(document)
|
||||
models = first["models"]
|
||||
assert isinstance(models, list)
|
||||
model = models[0]
|
||||
assert isinstance(model, dict)
|
||||
artifacts = model["artifacts"]
|
||||
assert isinstance(artifacts, list)
|
||||
artifact = artifacts[1]
|
||||
assert isinstance(artifact, dict)
|
||||
artifact["sha256"] = "0" * 64
|
||||
|
||||
with pytest.raises(
|
||||
PortableRunDefinitionRegistryError,
|
||||
match="portable definition digest changed",
|
||||
):
|
||||
PortableRunDefinitionRegistry.from_file(_write(tmp_path, document))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"forbidden_key",
|
||||
[
|
||||
"source_session_id",
|
||||
"source_label",
|
||||
"runner_path",
|
||||
"command",
|
||||
"environment",
|
||||
"user_priority",
|
||||
],
|
||||
)
|
||||
def test_registry_rejects_source_binding_and_executable_or_priority_fields(
|
||||
tmp_path: Path,
|
||||
forbidden_key: str,
|
||||
) -> None:
|
||||
document = _document()
|
||||
first = _first(document)
|
||||
requirements = first["source_requirements"]
|
||||
assert isinstance(requirements, dict)
|
||||
requirements[forbidden_key] = "caller-owned"
|
||||
|
||||
with pytest.raises(PortableRunDefinitionRegistryError, match="forbids"):
|
||||
PortableRunDefinitionRegistry.from_file(_write(tmp_path, document))
|
||||
|
||||
|
||||
def test_registry_rejects_authority_escalation_and_calibration_disagreement(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
authority_escalation = _document()
|
||||
first = _first(authority_escalation)
|
||||
authority = first["authority"]
|
||||
assert isinstance(authority, dict)
|
||||
authority["commands_enabled"] = True
|
||||
with pytest.raises(PortableRunDefinitionRegistryError, match="observation-only"):
|
||||
PortableRunDefinitionRegistry.from_file(_write(tmp_path, authority_escalation))
|
||||
|
||||
calibration_drift = _document()
|
||||
first = _first(calibration_drift)
|
||||
requirements = first["source_requirements"]
|
||||
assert isinstance(requirements, dict)
|
||||
requirements["calibration_identity_sha256"] = "a" * 64
|
||||
adapter = first["source_adapter"]
|
||||
assert isinstance(adapter, dict)
|
||||
adapter_document = (
|
||||
_registry()
|
||||
.definitions[0]
|
||||
.source_adapter.identity_document(_registry().definitions[0].source_requirements)
|
||||
)
|
||||
calibration = adapter_document["calibration"]
|
||||
assert isinstance(calibration, dict)
|
||||
calibration["sha256"] = "a" * 64
|
||||
adapter["contract_sha256"] = canonical_sha256(adapter_document)
|
||||
with pytest.raises(PortableRunDefinitionRegistryError, match="calibration"):
|
||||
PortableRunDefinitionRegistry.from_file(_write(tmp_path, calibration_drift))
|
||||
|
||||
|
||||
def test_duplicate_definition_and_incomplete_ready_executor_are_rejected(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
duplicate = _document()
|
||||
definitions = duplicate["definitions"]
|
||||
assert isinstance(definitions, list)
|
||||
definitions.append(copy.deepcopy(definitions[0]))
|
||||
with pytest.raises(PortableRunDefinitionRegistryError, match="setup IDs"):
|
||||
PortableRunDefinitionRegistry.from_file(_write(tmp_path, duplicate))
|
||||
|
||||
incomplete = _document()
|
||||
first = _first(incomplete)
|
||||
executor = first["executor"]
|
||||
assert isinstance(executor, dict)
|
||||
executor["state"] = "ready"
|
||||
executor["reason_code"] = None
|
||||
executor["reason"] = None
|
||||
with pytest.raises(PortableRunDefinitionRegistryError, match="release identity"):
|
||||
PortableRunDefinitionRegistry.from_file(_write(tmp_path, incomplete))
|
||||
|
||||
|
||||
def test_production_definition_is_blocked_until_release_and_image_are_sealed() -> None:
|
||||
registry = _registry()
|
||||
definition = registry.definitions[0]
|
||||
|
||||
assert definition.executor.state == "not-installed"
|
||||
assert definition.executor.release_id is None
|
||||
assert definition.executor.release_sha256 is None
|
||||
assert definition.executor.image_sha256 is None
|
||||
with pytest.raises(
|
||||
PortableRunDefinitionUnavailableError,
|
||||
match="not sealed or installed",
|
||||
):
|
||||
definition.to_recorded_run_definition()
|
||||
with pytest.raises(PortableRunDefinitionUnavailableError):
|
||||
registry.to_recorded_registry()
|
||||
|
||||
|
||||
def test_conversion_to_recorded_definition_requires_and_preserves_sealed_identities() -> None:
|
||||
blocked = _registry().definitions[0]
|
||||
ready_executor = PortableExecutorAvailability(
|
||||
contour_id="worker-006",
|
||||
state="ready",
|
||||
release_id="lab-v1-eomt-ddrnet-executor-v1",
|
||||
release_sha256="1" * 64,
|
||||
image_sha256="2" * 64,
|
||||
reason_code=None,
|
||||
reason=None,
|
||||
)
|
||||
identity = blocked.identity_document()
|
||||
identity["executor"] = ready_executor.identity_document()
|
||||
ready = replace(
|
||||
blocked,
|
||||
executor=ready_executor,
|
||||
definition_sha256=canonical_sha256(identity),
|
||||
)
|
||||
|
||||
recorded = ready.to_recorded_run_definition()
|
||||
|
||||
assert recorded.setup_id == ready.setup_id
|
||||
assert recorded.definition_sha256 == ready.definition_sha256
|
||||
assert recorded.source_adapter_sha256 == ready.source_adapter.contract_sha256
|
||||
assert recorded.executor_release_sha256 == "1" * 64
|
||||
assert recorded.executor_image_sha256 == "2" * 64
|
||||
assert recorded.model_release_ids == ready.learned_models
|
||||
assert recorded.model_manifest_sha256 == MODEL_MANIFEST_SHA256
|
||||
assert recorded.resource_profile_sha256 == ready.resource_profile.profile_sha256
|
||||
assert recorded.checkpoint_policy == "non-checkpointable"
|
||||
|
||||
|
||||
def test_production_lab_v1_model_component_and_result_identities_are_exact() -> None:
|
||||
definition = _registry().definitions[0]
|
||||
models = {model.release_id: model for model in definition.models}
|
||||
eomt = models["eomt-cityscapes-large-1024-v1"]
|
||||
ddrnet = models["lab-v1-ddrnet-39-goose-fine-64-v1"]
|
||||
|
||||
assert eomt.model_id == "tue-mps/cityscapes_semantic_eomt_large_1024"
|
||||
assert eomt.revision == "8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f"
|
||||
assert {artifact.role: artifact.sha256 for artifact in eomt.artifacts} == {
|
||||
"config-json": ("7f4aa94fa4e43c0dbd79a5420edb511120aef62bd82bfbcbcece79948286a650"),
|
||||
"model-weights": "c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782",
|
||||
"preprocessor-config": ("97e2fbf7f0bdba2cfc90251c5133bae9c27ddc9c4410509f40670be2332854e7"),
|
||||
}
|
||||
assert ddrnet.model_id == "goose-ddrnet-class-512"
|
||||
assert ddrnet.revision is None
|
||||
assert ddrnet.architecture == "ddrnet_39"
|
||||
assert ddrnet.artifacts[0].sha256 == (
|
||||
"b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6"
|
||||
)
|
||||
assert ddrnet.artifacts[0].byte_length == 259_419_077
|
||||
|
||||
components = {component.component_id: component for component in definition.components}
|
||||
assert components["eomt-recorded-profile-v1"].sha256 == (
|
||||
"ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875"
|
||||
)
|
||||
assert components["eomt-recorded-runner-v1"].sha256 == (
|
||||
"651e8e06c3912dffb036b7fd08f2c0623f7563d8306cc7aee05db562798518f4"
|
||||
)
|
||||
assert components["k1-camera-1-calibration-v1"].sha256 == (
|
||||
"05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||
)
|
||||
assert components["k1-valid-fov-identity-v1"].sha256 == (
|
||||
"b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2"
|
||||
)
|
||||
assert components["k1-valid-fov-mask-v1"].sha256 == (
|
||||
"a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63"
|
||||
)
|
||||
|
||||
assert definition.result_contract.result_schema == (
|
||||
"missioncore.recorded-eomt-ddrnet-review/v2"
|
||||
)
|
||||
assert definition.result_contract.result_kind == "recorded-perception-qualification"
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from k1link.observatory.portable_run_definitions import PortableRunDefinitionRegistry
|
||||
from k1link.observatory.portable_setup_projection import PortableLabV1SetupProjector
|
||||
from k1link.observatory.source_admission import PortableRecordedSourceCapability
|
||||
from k1link.sessions import SessionNotFoundError
|
||||
from k1link.sessions.models import SessionSummary
|
||||
from k1link.web.observatory_api import build_observatory_router
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json"
|
||||
SOURCE_SESSION_ID = "20260831T080000Z_viewer_live"
|
||||
|
||||
|
||||
def _summary() -> SessionSummary:
|
||||
return SessionSummary(
|
||||
session_id=SOURCE_SESSION_ID,
|
||||
display_name="RAVNOVES005",
|
||||
status="ready",
|
||||
started_at_utc="2026-08-31T08:00:00Z",
|
||||
completed_at_utc="2026-08-31T08:10:00Z",
|
||||
duration_seconds=600.0,
|
||||
modalities=("point-cloud", "trajectory", "video"),
|
||||
source_count=3,
|
||||
total_bytes=1,
|
||||
replayable=True,
|
||||
origin="recorded",
|
||||
)
|
||||
|
||||
|
||||
class _Store:
|
||||
def get_session(self, session_id: str) -> SimpleNamespace:
|
||||
if session_id != SOURCE_SESSION_ID:
|
||||
raise SessionNotFoundError(session_id)
|
||||
return SimpleNamespace(summary=_summary())
|
||||
|
||||
|
||||
def _projector() -> PortableLabV1SetupProjector:
|
||||
registry = PortableRunDefinitionRegistry.from_file(REGISTRY_PATH)
|
||||
definition = registry.definitions[0]
|
||||
|
||||
def probe(session_id: str) -> PortableRecordedSourceCapability:
|
||||
return PortableRecordedSourceCapability(
|
||||
source_session_id=session_id,
|
||||
source_catalog_sha256="a" * 64,
|
||||
source_adapter_sha256=definition.source_adapter.contract_sha256,
|
||||
camera_segment_count=600,
|
||||
)
|
||||
|
||||
return PortableLabV1SetupProjector(
|
||||
registry=registry,
|
||||
capability_probe=probe,
|
||||
)
|
||||
|
||||
|
||||
def test_portable_setup_catalog_exposes_capability_and_executor_separately() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
build_observatory_router(
|
||||
_Store(), # type: ignore[arg-type]
|
||||
portable_setup_projector=_projector(),
|
||||
)
|
||||
)
|
||||
|
||||
response = TestClient(app).get(
|
||||
"/api/v1/observatory/portable-laboratory-setups",
|
||||
params={"source_session_id": SOURCE_SESSION_ID},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
document = response.json()
|
||||
assert document["schema_version"] == ("missioncore.observatory-portable-setup-catalog/v2")
|
||||
setup = document["setups"][0]
|
||||
assert setup["origin"] == "portable-definition"
|
||||
assert setup["source_compatibility"]["outcome"] == "pass"
|
||||
assert setup["executor"]["state"] == "not-installed"
|
||||
assert setup["existing_results"] == []
|
||||
assert setup["preflight"]["outcome"] == "blocked"
|
||||
assert setup["preflight"]["submission_allowed"] is False
|
||||
|
||||
|
||||
def test_portable_setup_catalog_preserves_source_and_optional_slice_failures() -> None:
|
||||
ready_app = FastAPI()
|
||||
ready_app.include_router(
|
||||
build_observatory_router(
|
||||
_Store(), # type: ignore[arg-type]
|
||||
portable_setup_projector=_projector(),
|
||||
)
|
||||
)
|
||||
missing = TestClient(ready_app).get(
|
||||
"/api/v1/observatory/portable-laboratory-setups",
|
||||
params={"source_session_id": "missing-session"},
|
||||
)
|
||||
assert missing.status_code == 404
|
||||
|
||||
unavailable_app = FastAPI()
|
||||
unavailable_app.include_router(
|
||||
build_observatory_router(
|
||||
_Store(), # type: ignore[arg-type]
|
||||
portable_setup_projector_error="drifted portable registry",
|
||||
)
|
||||
)
|
||||
unavailable = TestClient(unavailable_app).get(
|
||||
"/api/v1/observatory/portable-laboratory-setups",
|
||||
params={"source_session_id": SOURCE_SESSION_ID},
|
||||
)
|
||||
assert unavailable.status_code == 503
|
||||
assert unavailable.json()["detail"] == "Portable-каталог LAB V1 недоступен."
|
||||
@@ -0,0 +1,244 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.observatory.portable_run_definitions import (
|
||||
PortableExecutorAvailability,
|
||||
PortableRunDefinitionRegistry,
|
||||
canonical_sha256,
|
||||
)
|
||||
from k1link.observatory.portable_setup_projection import (
|
||||
PORTABLE_LAB_V1_DISPLAY_NAME,
|
||||
PORTABLE_LABORATORY_SETUP_CATALOG_SCHEMA,
|
||||
PortableLabV1SetupProjector,
|
||||
PortableSetupProjectionError,
|
||||
PortableSourceCapabilityProbe,
|
||||
)
|
||||
from k1link.observatory.source_admission import (
|
||||
PortableRecordedSourceCapability,
|
||||
PortableSourceAdmissionIntegrityError,
|
||||
)
|
||||
from k1link.sessions.models import SessionSummary
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
REGISTRY_PATH = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json"
|
||||
RAV004_SESSION_ID = "20260828T130511Z_viewer_live"
|
||||
NEW_SESSION_ID = "20260831T080000Z_viewer_live"
|
||||
|
||||
|
||||
def _registry() -> PortableRunDefinitionRegistry:
|
||||
return PortableRunDefinitionRegistry.from_file(REGISTRY_PATH)
|
||||
|
||||
|
||||
def _source(
|
||||
session_id: str,
|
||||
*,
|
||||
display_name: str = "arbitrary operator label",
|
||||
) -> SessionSummary:
|
||||
return SessionSummary(
|
||||
session_id=session_id,
|
||||
display_name=display_name,
|
||||
status="ready",
|
||||
started_at_utc="2026-08-31T08:00:00Z",
|
||||
completed_at_utc="2026-08-31T08:10:00Z",
|
||||
duration_seconds=600.0,
|
||||
modalities=("point-cloud", "trajectory", "video"),
|
||||
source_count=3,
|
||||
total_bytes=1,
|
||||
replayable=True,
|
||||
origin="recorded",
|
||||
)
|
||||
|
||||
|
||||
def _capability(
|
||||
source_session_id: str,
|
||||
*,
|
||||
adapter_sha256: str | None = None,
|
||||
) -> PortableRecordedSourceCapability:
|
||||
definition = _registry().definitions[0]
|
||||
return PortableRecordedSourceCapability(
|
||||
source_session_id=source_session_id,
|
||||
source_catalog_sha256="a" * 64,
|
||||
source_adapter_sha256=(adapter_sha256 or definition.source_adapter.contract_sha256),
|
||||
camera_segment_count=60,
|
||||
)
|
||||
|
||||
|
||||
def _projector(
|
||||
capability_probe: PortableSourceCapabilityProbe,
|
||||
*,
|
||||
registry: PortableRunDefinitionRegistry | None = None,
|
||||
) -> PortableLabV1SetupProjector:
|
||||
return PortableLabV1SetupProjector(
|
||||
registry=registry or _registry(),
|
||||
capability_probe=capability_probe,
|
||||
)
|
||||
|
||||
|
||||
def test_projection_uses_portable_identity_and_exact_model_presentation() -> None:
|
||||
source = _source(NEW_SESSION_ID, display_name="RAVNOVES005")
|
||||
projection = _projector(lambda session_id: _capability(session_id)).catalog(source)
|
||||
setup = projection["setups"][0]
|
||||
|
||||
assert projection["schema_version"] == PORTABLE_LABORATORY_SETUP_CATALOG_SCHEMA
|
||||
assert setup["origin"] == "portable-definition"
|
||||
assert setup["display_name"] == PORTABLE_LAB_V1_DISPLAY_NAME
|
||||
assert setup["run_definition"]["models"] == [
|
||||
{
|
||||
"name": "EoMT Cityscapes Large 1024",
|
||||
"release_id": "eomt-cityscapes-large-1024-v1",
|
||||
"model_id": "tue-mps/cityscapes_semantic_eomt_large_1024",
|
||||
"architecture": "EomtForUniversalSegmentation",
|
||||
},
|
||||
{
|
||||
"name": "DDRNet-39",
|
||||
"release_id": "lab-v1-ddrnet-39-goose-fine-64-v1",
|
||||
"model_id": "goose-ddrnet-class-512",
|
||||
"architecture": "ddrnet_39",
|
||||
},
|
||||
]
|
||||
requirements = setup["source_requirements"]
|
||||
assert "source_session_id" not in requirements
|
||||
assert "source_label" not in requirements
|
||||
assert "label" not in requirements
|
||||
assert "RAVNOVES" not in str(requirements)
|
||||
assert setup["run_definition"]["definition_sha256"] == (
|
||||
_registry().definitions[0].definition_sha256
|
||||
)
|
||||
assert setup["run_definition"]["result_schema"] == (
|
||||
_registry().definitions[0].result_contract.result_schema
|
||||
)
|
||||
assert setup["authority"] == {
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"production_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
def test_new_compatible_source_passes_capability_but_uninstalled_executor_blocks() -> None:
|
||||
source = _source(NEW_SESSION_ID)
|
||||
setup = _projector(lambda session_id: _capability(session_id)).project(source)
|
||||
|
||||
assert setup["source_compatibility"] == {
|
||||
"outcome": "pass",
|
||||
"compatible": True,
|
||||
"reason": "Запись соответствует требованиям EoMT + DDRNet.",
|
||||
}
|
||||
assert setup["executor"]["state"] == "not-installed"
|
||||
assert setup["executor"]["ready"] is False
|
||||
assert setup["existing_results"] == []
|
||||
assert setup["preflight"] == {
|
||||
"outcome": "blocked",
|
||||
"action": "blocked",
|
||||
"reason": "Вычислительный контур LAB V1 пока недоступен.",
|
||||
"submission_allowed": False,
|
||||
"existing_result_ids": [],
|
||||
}
|
||||
|
||||
|
||||
def test_legacy_vegetation_result_is_never_existing_for_portable_v2() -> None:
|
||||
setup = _projector(lambda session_id: _capability(session_id)).project(
|
||||
_source(RAV004_SESSION_ID)
|
||||
)
|
||||
|
||||
assert setup["run_definition"]["result_schema"] == (
|
||||
"missioncore.recorded-eomt-ddrnet-review/v2"
|
||||
)
|
||||
assert setup["existing_results"] == []
|
||||
assert setup["preflight"] == {
|
||||
"outcome": "blocked",
|
||||
"action": "blocked",
|
||||
"reason": "Вычислительный контур LAB V1 пока недоступен.",
|
||||
"submission_allowed": False,
|
||||
"existing_result_ids": [],
|
||||
}
|
||||
|
||||
|
||||
class _RejectingCapabilityService:
|
||||
def probe(self, source_session_id: str) -> PortableRecordedSourceCapability:
|
||||
del source_session_id
|
||||
raise PortableSourceAdmissionIntegrityError("missing sealed video")
|
||||
|
||||
|
||||
def test_real_capability_probe_rejection_wins_over_summary() -> None:
|
||||
setup = _projector(_RejectingCapabilityService()).project(_source(RAV004_SESSION_ID))
|
||||
|
||||
assert setup["source_compatibility"] == {
|
||||
"outcome": "blocked",
|
||||
"compatible": False,
|
||||
"reason": "Запись не соответствует требованиям EoMT + DDRNet.",
|
||||
}
|
||||
assert setup["existing_results"] == []
|
||||
assert setup["preflight"]["outcome"] == "blocked"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mismatch", ["source", "adapter"])
|
||||
def test_capability_probe_must_match_source_and_definition_adapter(
|
||||
mismatch: str,
|
||||
) -> None:
|
||||
source = _source(NEW_SESSION_ID)
|
||||
|
||||
def probe(source_session_id: str) -> PortableRecordedSourceCapability:
|
||||
if mismatch == "source":
|
||||
return _capability(RAV004_SESSION_ID)
|
||||
return _capability(source_session_id, adapter_sha256="f" * 64)
|
||||
|
||||
with pytest.raises(PortableSetupProjectionError, match=mismatch):
|
||||
_projector(probe).project(source)
|
||||
|
||||
|
||||
class _ProbeOnlyCapabilityService:
|
||||
def probe(self, source_session_id: str) -> PortableRecordedSourceCapability:
|
||||
return _capability(source_session_id)
|
||||
|
||||
def check(self, source_session_id: str) -> None:
|
||||
raise AssertionError(f"full admission check called for {source_session_id}")
|
||||
|
||||
|
||||
def test_projector_uses_lightweight_probe_and_never_full_admission_check() -> None:
|
||||
setup = _projector(_ProbeOnlyCapabilityService()).project(_source(NEW_SESSION_ID))
|
||||
|
||||
assert setup["source_compatibility"]["outcome"] == "pass"
|
||||
|
||||
|
||||
def _ready_registry() -> PortableRunDefinitionRegistry:
|
||||
blocked = _registry().definitions[0]
|
||||
ready_executor = PortableExecutorAvailability(
|
||||
contour_id="worker-006",
|
||||
state="ready",
|
||||
release_id="lab-v1-eomt-ddrnet-executor-v1",
|
||||
release_sha256="1" * 64,
|
||||
image_sha256="2" * 64,
|
||||
reason_code=None,
|
||||
reason=None,
|
||||
)
|
||||
identity = blocked.identity_document()
|
||||
identity["executor"] = ready_executor.identity_document()
|
||||
ready = replace(
|
||||
blocked,
|
||||
executor=ready_executor,
|
||||
definition_sha256=canonical_sha256(identity),
|
||||
)
|
||||
return PortableRunDefinitionRegistry((ready,))
|
||||
|
||||
|
||||
def test_ready_executor_still_blocks_without_a_dispatch_boundary() -> None:
|
||||
setup = _projector(
|
||||
lambda session_id: _capability(session_id),
|
||||
registry=_ready_registry(),
|
||||
).project(_source(NEW_SESSION_ID))
|
||||
|
||||
assert setup["preflight"] == {
|
||||
"outcome": "blocked",
|
||||
"action": "blocked",
|
||||
"reason": (
|
||||
"Server-side проверка definition/check SHA и постановка portable "
|
||||
"LAB V1 в очередь пока недоступны."
|
||||
),
|
||||
"submission_allowed": False,
|
||||
"existing_result_ids": [],
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.observatory.source_admission import (
|
||||
PORTABLE_SOURCE_BUNDLE_SCHEMA,
|
||||
PORTABLE_SOURCE_CAPABILITY_SCHEMA,
|
||||
PortableSourceAdmissionIntegrityError,
|
||||
PortableSourceAdmissionStaleError,
|
||||
PortableSourceNotPreparedError,
|
||||
RecordedK1SourceAdmissionService,
|
||||
RecordedK1SourceRequirements,
|
||||
)
|
||||
from k1link.sessions.media import (
|
||||
CAMERA_ARCHIVE_SCHEMA,
|
||||
RecordedMediaEpoch,
|
||||
RecordedMediaManifest,
|
||||
RecordedMediaSegment,
|
||||
)
|
||||
from k1link.sessions.models import (
|
||||
LabReplayCapability,
|
||||
LabSessionBinding,
|
||||
RecordedMediaArtifact,
|
||||
ReplayArtifact,
|
||||
ReplayCommand,
|
||||
SessionArtifact,
|
||||
SessionDetail,
|
||||
SessionSource,
|
||||
SessionSummary,
|
||||
)
|
||||
|
||||
INIT_SHA256 = "e2279963e16d84c91d68e7dbb1f7efed840533387dfeb844b7398bff45fbde38"
|
||||
CALIBRATION_SHA256 = "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||
CATALOG_SHA256 = "1" * 64
|
||||
RAW_SHA256 = "2" * 64
|
||||
GENERATION_SHA256 = "3" * 64
|
||||
SEGMENT_SHA256 = "4" * 64
|
||||
|
||||
|
||||
def _requirements() -> RecordedK1SourceRequirements:
|
||||
return RecordedK1SourceRequirements(
|
||||
adapter_id="recorded-k1-right-camera",
|
||||
adapter_version=1,
|
||||
plugin_id="nodedc.device.xgrids-lixelkity-k1",
|
||||
archive_id="xgrids-k1.viewer-live.evidence",
|
||||
required_modalities=("point-cloud", "trajectory", "video"),
|
||||
camera_source_id="sensor.camera.right",
|
||||
camera_semantic_channel_id="camera.video.recorded",
|
||||
camera_media_type='video/mp4; codecs="avc1.641028"',
|
||||
camera_init_sha256=INIT_SHA256,
|
||||
expected_width=800,
|
||||
expected_height=600,
|
||||
calibration_slot="camera_1",
|
||||
calibration_sha256=CALIBRATION_SHA256,
|
||||
)
|
||||
|
||||
|
||||
def _detail(session_id: str, display_name: str) -> SessionDetail:
|
||||
return SessionDetail(
|
||||
summary=SessionSummary(
|
||||
session_id=session_id,
|
||||
display_name=display_name,
|
||||
status="ready",
|
||||
started_at_utc="2026-08-31T00:00:00Z",
|
||||
completed_at_utc="2026-08-31T00:10:00Z",
|
||||
duration_seconds=600.0,
|
||||
modalities=("point-cloud", "trajectory", "video"),
|
||||
source_count=3,
|
||||
total_bytes=123,
|
||||
replayable=True,
|
||||
origin="xgrids-k1.viewer-live.evidence",
|
||||
),
|
||||
sources=(
|
||||
SessionSource(
|
||||
source_id="sensor.camera.right",
|
||||
semantic_channel_id="camera.video.recorded",
|
||||
modality="video",
|
||||
status="recorded",
|
||||
seekable=True,
|
||||
artifact_id="recorded-video-right",
|
||||
),
|
||||
SessionSource(
|
||||
source_id="sensor.lidar.primary",
|
||||
semantic_channel_id="spatial.point-cloud.recorded",
|
||||
modality="point-cloud",
|
||||
status="recorded",
|
||||
seekable=True,
|
||||
artifact_id="raw-transport-primary",
|
||||
),
|
||||
SessionSource(
|
||||
source_id="spatial.trajectory",
|
||||
semantic_channel_id="spatial.pose.recorded",
|
||||
modality="trajectory",
|
||||
status="recorded",
|
||||
seekable=True,
|
||||
artifact_id="raw-transport-primary",
|
||||
),
|
||||
),
|
||||
artifacts=(
|
||||
SessionArtifact(
|
||||
artifact_id="raw-transport-primary",
|
||||
kind="raw-transport",
|
||||
media_type="application/x-nodedc-k1mqtt",
|
||||
byte_length=100,
|
||||
sha256=RAW_SHA256,
|
||||
integrity_status="verified",
|
||||
),
|
||||
SessionArtifact(
|
||||
artifact_id="recorded-video-right",
|
||||
kind="recorded-video",
|
||||
media_type="video/mp4",
|
||||
byte_length=20,
|
||||
sha256=None,
|
||||
integrity_status="validated-structure",
|
||||
),
|
||||
),
|
||||
plugin_id="nodedc.device.xgrids-lixelkity-k1",
|
||||
archive_id="xgrids-k1.viewer-live.evidence",
|
||||
)
|
||||
|
||||
|
||||
def _replay(session_id: str, root: Path) -> ReplayCommand:
|
||||
return ReplayCommand(
|
||||
session_id=session_id,
|
||||
plugin_id="nodedc.device.xgrids-lixelkity-k1",
|
||||
allowed_root=root,
|
||||
session_root=root / session_id,
|
||||
primary_artifact_id="raw-transport-primary",
|
||||
artifacts=(
|
||||
ReplayArtifact(
|
||||
artifact_id="raw-transport-primary",
|
||||
path=root / "mqtt.raw.k1mqtt",
|
||||
media_type="application/x-nodedc-k1mqtt",
|
||||
file_byte_length=100,
|
||||
replay_byte_length=100,
|
||||
expected_sha256=RAW_SHA256,
|
||||
),
|
||||
),
|
||||
timeline_origin_epoch_ns=1,
|
||||
timeline_origin_monotonic_ns=2,
|
||||
speed=1.0,
|
||||
loop=False,
|
||||
)
|
||||
|
||||
|
||||
def _recorded_media(session_id: str, root: Path) -> RecordedMediaArtifact:
|
||||
return RecordedMediaArtifact(
|
||||
session_id=session_id,
|
||||
public_source_id="recorded.camera.right",
|
||||
artifact_id="recorded-video-right",
|
||||
source_path=root / "media" / "sensor.camera.right",
|
||||
byte_length=20,
|
||||
)
|
||||
|
||||
|
||||
def _manifest(session_id: str, root: Path) -> RecordedMediaManifest:
|
||||
segment = RecordedMediaSegment(
|
||||
sequence=1,
|
||||
path=root / "epoch-1" / "segments" / "1.m4s",
|
||||
byte_length=10,
|
||||
sha256=SEGMENT_SHA256,
|
||||
random_access=True,
|
||||
end_time_seconds=0.1,
|
||||
)
|
||||
epoch = RecordedMediaEpoch(
|
||||
ordinal=1,
|
||||
path=root / "epoch-1",
|
||||
init_path=root / "epoch-1" / "init.mp4",
|
||||
init_byte_length=10,
|
||||
init_sha256=INIT_SHA256,
|
||||
media_type='video/mp4; codecs="avc1.641028"',
|
||||
timeline_start_seconds=10.0,
|
||||
timeline_end_seconds=10.1,
|
||||
segments=(segment,),
|
||||
)
|
||||
return RecordedMediaManifest(
|
||||
session_id=session_id,
|
||||
public_source_id="recorded.camera.right",
|
||||
artifact_id="recorded-video-right",
|
||||
synchronization="host-arrival-best-effort",
|
||||
generation_sha256=GENERATION_SHA256,
|
||||
timeline_start_seconds=10.0,
|
||||
timeline_end_seconds=10.1,
|
||||
byte_length=20,
|
||||
epochs=(epoch,),
|
||||
)
|
||||
|
||||
|
||||
class _Store:
|
||||
def __init__(
|
||||
self,
|
||||
root: Path,
|
||||
detail: SessionDetail,
|
||||
*,
|
||||
catalogs: tuple[str, ...] = (CATALOG_SHA256,),
|
||||
) -> None:
|
||||
self.data_dir = root
|
||||
self.detail = detail
|
||||
self.replay = _replay(detail.summary.session_id, root)
|
||||
self.media = (_recorded_media(detail.summary.session_id, root),)
|
||||
self.catalogs = catalogs
|
||||
self.catalog_reads = 0
|
||||
self.prepare_replay_calls = 0
|
||||
self.recorded_media_reads = 0
|
||||
|
||||
def get_session_with_catalog_snapshot(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> tuple[SessionDetail, str]:
|
||||
assert session_id == self.detail.summary.session_id
|
||||
index = min(self.catalog_reads, len(self.catalogs) - 1)
|
||||
self.catalog_reads += 1
|
||||
return self.detail, self.catalogs[index]
|
||||
|
||||
def prepare_replay(self, session_id: str) -> ReplayCommand:
|
||||
assert session_id == self.detail.summary.session_id
|
||||
self.prepare_replay_calls += 1
|
||||
return self.replay
|
||||
|
||||
def list_recorded_media(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> tuple[RecordedMediaArtifact, ...]:
|
||||
assert session_id == self.detail.summary.session_id
|
||||
self.recorded_media_reads += 1
|
||||
return self.media
|
||||
|
||||
|
||||
class _Inspector:
|
||||
def __init__(self, manifest: RecordedMediaManifest | None) -> None:
|
||||
self.manifest = manifest
|
||||
self.inspect_calls = 0
|
||||
self.restore_calls = 0
|
||||
|
||||
def inspect(
|
||||
self,
|
||||
artifact: RecordedMediaArtifact,
|
||||
replay: ReplayCommand,
|
||||
) -> RecordedMediaManifest | None:
|
||||
assert artifact.session_id == replay.session_id
|
||||
self.inspect_calls += 1
|
||||
return self.manifest
|
||||
|
||||
def restore_prepared(
|
||||
self,
|
||||
artifact: RecordedMediaArtifact,
|
||||
replay: ReplayCommand,
|
||||
) -> RecordedMediaManifest | None:
|
||||
assert artifact.session_id == replay.session_id
|
||||
self.restore_calls += 1
|
||||
return self.manifest
|
||||
|
||||
|
||||
def _write_probe_summary(root: Path, *, segment_count: int = 7) -> None:
|
||||
epoch = root / "media" / "sensor.camera.right" / "epoch-1"
|
||||
epoch.mkdir(parents=True)
|
||||
(epoch / "summary.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": CAMERA_ARCHIVE_SCHEMA,
|
||||
"source_id": "sensor.camera.right",
|
||||
"codec_epoch": 1,
|
||||
"status": "complete",
|
||||
"segment_count": segment_count,
|
||||
"entry_count": segment_count,
|
||||
"media_segment_count": segment_count,
|
||||
"init_sha256": INIT_SHA256,
|
||||
"synchronization": "host-arrival-best-effort",
|
||||
"commit_policy": "per-segment-fsync",
|
||||
"failure_code": None,
|
||||
"artifacts": {
|
||||
"init": "init.mp4",
|
||||
"segments": "segments",
|
||||
"index": "index.jsonl",
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _service(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
session_id: str = "20260831T000000Z_viewer_live",
|
||||
display_name: str = "RAVNOVES005",
|
||||
detail: SessionDetail | None = None,
|
||||
manifest: RecordedMediaManifest | None = None,
|
||||
) -> RecordedK1SourceAdmissionService:
|
||||
active_detail = detail or _detail(session_id, display_name)
|
||||
store = _Store(tmp_path, active_detail)
|
||||
return RecordedK1SourceAdmissionService(
|
||||
data_dir=tmp_path,
|
||||
session_store=store, # type: ignore[arg-type]
|
||||
media_inspector=_Inspector(
|
||||
manifest or _manifest(active_detail.summary.session_id, tmp_path)
|
||||
), # type: ignore[arg-type]
|
||||
requirements=_requirements(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("session_id", "display_name"),
|
||||
(
|
||||
("20260720T065719Z_viewer_live", "RAVNOVES00"),
|
||||
("20260828T130511Z_viewer_live", "RAVNOVES004TREE"),
|
||||
("20260831T000000Z_viewer_live", "RAVNOVES005"),
|
||||
),
|
||||
)
|
||||
def test_portable_source_admission_is_independent_from_session_label(
|
||||
tmp_path: Path,
|
||||
session_id: str,
|
||||
display_name: str,
|
||||
) -> None:
|
||||
admission = _service(
|
||||
tmp_path,
|
||||
session_id=session_id,
|
||||
display_name=display_name,
|
||||
).check(session_id)
|
||||
|
||||
assert admission.source_session_id == session_id
|
||||
assert admission.frame_count == 1
|
||||
bundle = json.loads(admission.source_bundle)
|
||||
capability = json.loads(admission.capability_manifest)
|
||||
assert bundle["schema_version"] == PORTABLE_SOURCE_BUNDLE_SCHEMA
|
||||
assert capability["schema_version"] == PORTABLE_SOURCE_CAPABILITY_SCHEMA
|
||||
assert "RAVNOVES" not in admission.source_bundle.decode()
|
||||
assert "path" not in admission.source_bundle.decode()
|
||||
assert capability["camera_profile"]["width"] == 800
|
||||
assert capability["camera_profile"]["height"] == 600
|
||||
|
||||
|
||||
def test_catalog_capability_check_restores_media_without_preparing_it(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
detail = _detail("20260831T000000Z_viewer_live", "RAVNOVES005")
|
||||
store = _Store(tmp_path, detail)
|
||||
inspector = _Inspector(_manifest(detail.summary.session_id, tmp_path))
|
||||
service = RecordedK1SourceAdmissionService(
|
||||
data_dir=tmp_path,
|
||||
session_store=store, # type: ignore[arg-type]
|
||||
media_inspector=inspector, # type: ignore[arg-type]
|
||||
requirements=_requirements(),
|
||||
prepare_media=False,
|
||||
)
|
||||
|
||||
admission = service.check(detail.summary.session_id)
|
||||
|
||||
assert admission.frame_count == 1
|
||||
assert inspector.inspect_calls == 0
|
||||
assert inspector.restore_calls == 1
|
||||
|
||||
|
||||
def test_lightweight_probe_reads_only_catalog_media_handle_and_summary(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
detail = _detail("20260831T000000Z_viewer_live", "ignored-label")
|
||||
store = _Store(tmp_path, detail)
|
||||
inspector = _Inspector(_manifest(detail.summary.session_id, tmp_path))
|
||||
_write_probe_summary(tmp_path, segment_count=23)
|
||||
service = RecordedK1SourceAdmissionService(
|
||||
data_dir=tmp_path,
|
||||
session_store=store, # type: ignore[arg-type]
|
||||
media_inspector=inspector, # type: ignore[arg-type]
|
||||
requirements=_requirements(),
|
||||
)
|
||||
|
||||
capability = service.probe(detail.summary.session_id)
|
||||
|
||||
assert capability.source_session_id == detail.summary.session_id
|
||||
assert capability.source_catalog_sha256 == CATALOG_SHA256
|
||||
assert capability.source_adapter_sha256 == _requirements().adapter_sha256
|
||||
assert capability.camera_segment_count == 23
|
||||
assert store.catalog_reads == 1
|
||||
assert store.recorded_media_reads == 1
|
||||
assert store.prepare_replay_calls == 0
|
||||
assert inspector.inspect_calls == 0
|
||||
assert inspector.restore_calls == 0
|
||||
assert not (tmp_path / "media" / "sensor.camera.right" / "epoch-1" / "segments").exists()
|
||||
assert not (tmp_path / "observatory-portable-source-contracts").exists()
|
||||
|
||||
|
||||
def test_read_only_check_reports_missing_prepared_manifest_without_writes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
detail = _detail("20260831T000000Z_viewer_live", "ignored-label")
|
||||
store = _Store(tmp_path, detail)
|
||||
inspector = _Inspector(None)
|
||||
service = RecordedK1SourceAdmissionService(
|
||||
data_dir=tmp_path,
|
||||
session_store=store, # type: ignore[arg-type]
|
||||
media_inspector=inspector, # type: ignore[arg-type]
|
||||
requirements=_requirements(),
|
||||
)
|
||||
|
||||
with pytest.raises(PortableSourceNotPreparedError, match="not been prepared"):
|
||||
service.check(detail.summary.session_id)
|
||||
|
||||
assert inspector.inspect_calls == 0
|
||||
assert inspector.restore_calls == 1
|
||||
assert not (tmp_path / "observatory-portable-source-contracts").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mutation", "message"),
|
||||
(
|
||||
("replay-session", "another source contract"),
|
||||
("replay-digest", "replay member disagrees"),
|
||||
("media-session", "artifact identity disagrees"),
|
||||
),
|
||||
)
|
||||
def test_full_check_rejects_cross_bound_replay_and_media(
|
||||
tmp_path: Path,
|
||||
mutation: str,
|
||||
message: str,
|
||||
) -> None:
|
||||
detail = _detail("20260831T000000Z_viewer_live", "ignored-label")
|
||||
store = _Store(tmp_path, detail)
|
||||
if mutation == "replay-session":
|
||||
store.replay = replace(store.replay, session_id="another-session")
|
||||
elif mutation == "replay-digest":
|
||||
store.replay = replace(
|
||||
store.replay,
|
||||
artifacts=(replace(store.replay.artifacts[0], expected_sha256="9" * 64),),
|
||||
)
|
||||
else:
|
||||
store.media = (replace(store.media[0], session_id="another-session"),)
|
||||
inspector = _Inspector(_manifest(detail.summary.session_id, tmp_path))
|
||||
service = RecordedK1SourceAdmissionService(
|
||||
data_dir=tmp_path,
|
||||
session_store=store, # type: ignore[arg-type]
|
||||
media_inspector=inspector, # type: ignore[arg-type]
|
||||
requirements=_requirements(),
|
||||
)
|
||||
|
||||
with pytest.raises(PortableSourceAdmissionIntegrityError, match=message):
|
||||
service.check(detail.summary.session_id)
|
||||
|
||||
assert inspector.inspect_calls == 0
|
||||
assert not (tmp_path / "observatory-portable-source-contracts").exists()
|
||||
|
||||
|
||||
def test_admit_rechecks_catalog_immediately_before_persistence(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
detail = _detail("20260831T000000Z_viewer_live", "ignored-label")
|
||||
store = _Store(tmp_path, detail, catalogs=(CATALOG_SHA256, "7" * 64))
|
||||
service = RecordedK1SourceAdmissionService(
|
||||
data_dir=tmp_path,
|
||||
session_store=store, # type: ignore[arg-type]
|
||||
media_inspector=_Inspector(_manifest(detail.summary.session_id, tmp_path)), # type: ignore[arg-type]
|
||||
requirements=_requirements(),
|
||||
)
|
||||
|
||||
with pytest.raises(PortableSourceAdmissionStaleError, match="changed"):
|
||||
service.admit(detail.summary.session_id)
|
||||
|
||||
assert store.catalog_reads == 2
|
||||
assert not (tmp_path / "observatory-portable-source-contracts").exists()
|
||||
|
||||
|
||||
def test_portable_source_admission_rejects_missing_video(tmp_path: Path) -> None:
|
||||
detail = _detail("20260728T163450Z_viewer_live", "RAVNOVES01")
|
||||
detail = replace(
|
||||
detail,
|
||||
summary=replace(
|
||||
detail.summary,
|
||||
modalities=("point-cloud", "trajectory"),
|
||||
source_count=2,
|
||||
),
|
||||
sources=tuple(source for source in detail.sources if source.modality != "video"),
|
||||
artifacts=tuple(
|
||||
artifact
|
||||
for artifact in detail.artifacts
|
||||
if artifact.artifact_id != "recorded-video-right"
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
PortableSourceAdmissionIntegrityError,
|
||||
match="modalities",
|
||||
):
|
||||
_service(tmp_path, detail=detail).check(detail.summary.session_id)
|
||||
|
||||
|
||||
def test_portable_source_admission_rejects_another_camera_profile(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
session_id = "20260831T000000Z_viewer_live"
|
||||
manifest = _manifest(session_id, tmp_path)
|
||||
manifest = replace(
|
||||
manifest,
|
||||
epochs=(replace(manifest.epochs[0], init_sha256="9" * 64),),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
PortableSourceAdmissionIntegrityError,
|
||||
match="media profile",
|
||||
):
|
||||
_service(tmp_path, session_id=session_id, manifest=manifest).check(session_id)
|
||||
|
||||
|
||||
def test_portable_source_admission_rejects_lab_chaining(tmp_path: Path) -> None:
|
||||
detail = _detail("20260831T000000Z_viewer_live", "Derived")
|
||||
lab = LabSessionBinding(
|
||||
session_id=detail.summary.session_id,
|
||||
source_session_id="20260828T130511Z_viewer_live",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-result",
|
||||
source_result_id=None,
|
||||
config_sha256=None,
|
||||
run_created_at_utc="2026-08-31T00:00:00Z",
|
||||
published_at_utc="2026-08-31T00:00:01Z",
|
||||
replay_capability=LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
),
|
||||
provenance={},
|
||||
)
|
||||
detail = replace(detail, summary=replace(detail.summary, lab=lab))
|
||||
|
||||
with pytest.raises(
|
||||
PortableSourceAdmissionIntegrityError,
|
||||
match="not an admitted",
|
||||
):
|
||||
_service(tmp_path, detail=detail).check(detail.summary.session_id)
|
||||
|
||||
|
||||
def test_portable_source_admission_persists_only_content_addressed_documents(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
session_id = "20260831T000000Z_viewer_live"
|
||||
service = _service(tmp_path, session_id=session_id)
|
||||
|
||||
first = service.admit(session_id)
|
||||
repeated = service.admit(session_id)
|
||||
|
||||
assert repeated == first
|
||||
root = tmp_path / "observatory-portable-source-contracts"
|
||||
assert sorted(path.name for path in root.iterdir()) == sorted(
|
||||
(
|
||||
f"{first.source_bundle_sha256}.json",
|
||||
f"{first.source_capability_manifest_sha256}.json",
|
||||
)
|
||||
)
|
||||
assert (root / f"{first.source_bundle_sha256}.json").read_bytes() == first.source_bundle
|
||||
assert (
|
||||
root / f"{first.source_capability_manifest_sha256}.json"
|
||||
).read_bytes() == first.capability_manifest
|
||||
@@ -0,0 +1,416 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from threading import Event, Thread
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.observatory.recorded_jobs import (
|
||||
ObservatoryRecordedJobIntent,
|
||||
ObservatoryRecordedJobQueue,
|
||||
RecordedRunDefinition,
|
||||
RecordedRunDefinitionRegistry,
|
||||
)
|
||||
from k1link.observatory.worker_agent import (
|
||||
MAX_EXECUTOR_FAILURE_MESSAGE_LENGTH,
|
||||
WORKER_006_CONTOUR_ID,
|
||||
ObservatoryWorkerAgent,
|
||||
ObservatoryWorkerAgentBusyError,
|
||||
ObservatoryWorkerExecutionResult,
|
||||
ObservatoryWorkerExecutorIdentity,
|
||||
ObservatoryWorkerExecutorRegistration,
|
||||
ObservatoryWorkerExecutorRegistry,
|
||||
SealedObservatoryRecordedJob,
|
||||
)
|
||||
|
||||
DEFINITION_SHA = "1" * 64
|
||||
ADAPTER_SHA = "2" * 64
|
||||
EXECUTOR_RELEASE_SHA = "3" * 64
|
||||
EXECUTOR_IMAGE_SHA = "4" * 64
|
||||
MODEL_MANIFEST_SHA = "5" * 64
|
||||
RESOURCE_PROFILE_SHA = "6" * 64
|
||||
CATALOG_SHA = "7" * 64
|
||||
SOURCE_BUNDLE_SHA = "8" * 64
|
||||
SOURCE_CAPABILITY_SHA = "9" * 64
|
||||
RESULT_SHA = "a" * 64
|
||||
|
||||
|
||||
def _definition() -> RecordedRunDefinition:
|
||||
return RecordedRunDefinition(
|
||||
setup_id="portable-lab-v1",
|
||||
definition_id="portable-lab-v1-definition",
|
||||
definition_version=1,
|
||||
definition_sha256=DEFINITION_SHA,
|
||||
source_adapter_id="sealed-session-source",
|
||||
source_adapter_version=1,
|
||||
source_adapter_sha256=ADAPTER_SHA,
|
||||
executor_release_id="portable-lab-v1-worker",
|
||||
executor_release_sha256=EXECUTOR_RELEASE_SHA,
|
||||
executor_image_sha256=EXECUTOR_IMAGE_SHA,
|
||||
model_release_ids=("eomt-cityscapes-large", "ddrnet-39"),
|
||||
model_manifest_sha256=MODEL_MANIFEST_SHA,
|
||||
resource_profile_id="worker006-single-gpu",
|
||||
resource_profile_sha256=RESOURCE_PROFILE_SHA,
|
||||
checkpoint_policy="cooperative",
|
||||
allowed_checkpoints=("semantic-pass",),
|
||||
)
|
||||
|
||||
|
||||
def _identity(
|
||||
*,
|
||||
release_sha256: str = EXECUTOR_RELEASE_SHA,
|
||||
) -> ObservatoryWorkerExecutorIdentity:
|
||||
return ObservatoryWorkerExecutorIdentity(
|
||||
release_sha256=release_sha256,
|
||||
image_sha256=EXECUTOR_IMAGE_SHA,
|
||||
model_manifest_sha256=MODEL_MANIFEST_SHA,
|
||||
resource_profile_sha256=RESOURCE_PROFILE_SHA,
|
||||
)
|
||||
|
||||
|
||||
def _queue(tmp_path: Path) -> ObservatoryRecordedJobQueue:
|
||||
return ObservatoryRecordedJobQueue(
|
||||
tmp_path,
|
||||
definitions=RecordedRunDefinitionRegistry((_definition(),)),
|
||||
)
|
||||
|
||||
|
||||
def _enqueue(queue: ObservatoryRecordedJobQueue) -> str:
|
||||
job, created = queue.submit(
|
||||
ObservatoryRecordedJobIntent(
|
||||
idempotency_key="worker-agent:test-job",
|
||||
source_session_id="20260831T120000Z_viewer_live",
|
||||
source_catalog_sha256=CATALOG_SHA,
|
||||
source_bundle_sha256=SOURCE_BUNDLE_SHA,
|
||||
source_capability_manifest_sha256=SOURCE_CAPABILITY_SHA,
|
||||
setup_id="portable-lab-v1",
|
||||
definition_sha256=DEFINITION_SHA,
|
||||
),
|
||||
enqueue=True,
|
||||
)
|
||||
assert created is True
|
||||
return job.job_id
|
||||
|
||||
|
||||
ClaimMutator = Callable[[dict[str, object]], dict[str, object]]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FakeTransport:
|
||||
queue: ObservatoryRecordedJobQueue
|
||||
claim_mutator: ClaimMutator | None = None
|
||||
starts: list[str] = field(default_factory=list)
|
||||
successes: list[tuple[str, str, str]] = field(default_factory=list)
|
||||
failures: list[tuple[str, str, str]] = field(default_factory=list)
|
||||
|
||||
def claim_next(
|
||||
self,
|
||||
*,
|
||||
claimant_id: str,
|
||||
claim_request_id: str,
|
||||
) -> Mapping[str, object] | None:
|
||||
claim = self.queue.claim_next(
|
||||
claimant_id=claimant_id,
|
||||
claim_request_id=claim_request_id,
|
||||
)
|
||||
if claim is None:
|
||||
return None
|
||||
payload = claim.as_dict()
|
||||
return self.claim_mutator(payload) if self.claim_mutator is not None else payload
|
||||
|
||||
def start(
|
||||
self,
|
||||
*,
|
||||
claimant_id: str,
|
||||
job_id: str,
|
||||
claim_token: str,
|
||||
) -> Mapping[str, object]:
|
||||
assert claimant_id == WORKER_006_CONTOUR_ID
|
||||
self.starts.append(job_id)
|
||||
return self.queue.start(job_id, claim_token=claim_token).as_dict()
|
||||
|
||||
def succeed(
|
||||
self,
|
||||
*,
|
||||
claimant_id: str,
|
||||
job_id: str,
|
||||
claim_token: str,
|
||||
result_id: str,
|
||||
result_sha256: str,
|
||||
) -> Mapping[str, object]:
|
||||
assert claimant_id == WORKER_006_CONTOUR_ID
|
||||
self.successes.append((job_id, result_id, result_sha256))
|
||||
return self.queue.succeed(
|
||||
job_id,
|
||||
claim_token=claim_token,
|
||||
result_id=result_id,
|
||||
result_sha256=result_sha256,
|
||||
).as_dict()
|
||||
|
||||
def fail(
|
||||
self,
|
||||
*,
|
||||
claimant_id: str,
|
||||
job_id: str,
|
||||
claim_token: str,
|
||||
error_code: str,
|
||||
message: str,
|
||||
) -> Mapping[str, object]:
|
||||
assert claimant_id == WORKER_006_CONTOUR_ID
|
||||
self.failures.append((job_id, error_code, message))
|
||||
return self.queue.fail(
|
||||
job_id,
|
||||
claim_token=claim_token,
|
||||
error_code=error_code,
|
||||
message=message,
|
||||
).as_dict()
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FakeExecutor:
|
||||
result: ObservatoryWorkerExecutionResult = field(
|
||||
default_factory=lambda: ObservatoryWorkerExecutionResult(
|
||||
result_id="lab-v1-result",
|
||||
result_sha256=RESULT_SHA,
|
||||
)
|
||||
)
|
||||
error: Exception | None = None
|
||||
jobs: list[SealedObservatoryRecordedJob] = field(default_factory=list)
|
||||
|
||||
def execute(
|
||||
self,
|
||||
job: SealedObservatoryRecordedJob,
|
||||
) -> ObservatoryWorkerExecutionResult:
|
||||
self.jobs.append(job)
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
return self.result
|
||||
|
||||
|
||||
def _agent(
|
||||
transport: FakeTransport,
|
||||
executor: FakeExecutor,
|
||||
*,
|
||||
identity: ObservatoryWorkerExecutorIdentity | None = None,
|
||||
) -> ObservatoryWorkerAgent:
|
||||
return ObservatoryWorkerAgent(
|
||||
transport=transport,
|
||||
executors=ObservatoryWorkerExecutorRegistry(
|
||||
(
|
||||
ObservatoryWorkerExecutorRegistration(
|
||||
identity=identity or _identity(),
|
||||
adapter=executor,
|
||||
),
|
||||
)
|
||||
),
|
||||
claim_request_id_factory=lambda: "worker-006:test-cycle",
|
||||
)
|
||||
|
||||
|
||||
def test_worker_agent_executes_one_sealed_allowlisted_job(tmp_path: Path) -> None:
|
||||
queue = _queue(tmp_path)
|
||||
job_id = _enqueue(queue)
|
||||
transport = FakeTransport(queue)
|
||||
executor = FakeExecutor()
|
||||
|
||||
report = _agent(transport, executor).run_once()
|
||||
|
||||
assert report.state == "succeeded"
|
||||
assert report.job_id == job_id
|
||||
assert report.result_id == "lab-v1-result"
|
||||
assert transport.starts == [job_id]
|
||||
assert transport.successes == [(job_id, "lab-v1-result", RESULT_SHA)]
|
||||
assert transport.failures == []
|
||||
assert len(executor.jobs) == 1
|
||||
sealed_job = executor.jobs[0]
|
||||
assert sealed_job.executor_identity == _identity()
|
||||
assert sealed_job.source_bundle_sha256 == SOURCE_BUNDLE_SHA
|
||||
assert not hasattr(sealed_job, "command")
|
||||
assert not hasattr(sealed_job, "path")
|
||||
assert not hasattr(sealed_job, "environment")
|
||||
assert queue.get(job_id).state == "succeeded"
|
||||
|
||||
|
||||
def test_worker_agent_leaves_empty_queue_untouched(tmp_path: Path) -> None:
|
||||
queue = _queue(tmp_path)
|
||||
transport = FakeTransport(queue)
|
||||
executor = FakeExecutor()
|
||||
|
||||
report = _agent(transport, executor).run_once()
|
||||
|
||||
assert report.state == "empty"
|
||||
assert report.job_id is None
|
||||
assert transport.starts == []
|
||||
assert transport.failures == []
|
||||
assert executor.jobs == []
|
||||
|
||||
|
||||
def test_exact_release_mismatch_fails_closed_without_start(tmp_path: Path) -> None:
|
||||
queue = _queue(tmp_path)
|
||||
job_id = _enqueue(queue)
|
||||
transport = FakeTransport(queue)
|
||||
executor = FakeExecutor()
|
||||
|
||||
report = _agent(
|
||||
transport,
|
||||
executor,
|
||||
identity=_identity(release_sha256="b" * 64),
|
||||
).run_once()
|
||||
|
||||
assert report.state == "failed"
|
||||
assert report.failure_code == "executor-not-allowlisted"
|
||||
assert transport.starts == []
|
||||
assert transport.successes == []
|
||||
assert transport.failures == [
|
||||
(
|
||||
job_id,
|
||||
"executor-not-allowlisted",
|
||||
"Exact executor identity is not installed on Worker 006.",
|
||||
)
|
||||
]
|
||||
assert executor.jobs == []
|
||||
assert queue.get(job_id).state == "failed"
|
||||
|
||||
|
||||
def _spoof_claimant(payload: dict[str, object]) -> dict[str, object]:
|
||||
changed = deepcopy(payload)
|
||||
changed["claimant_id"] = "worker-007"
|
||||
return changed
|
||||
|
||||
|
||||
def _inject_unknown_execution_payload(payload: dict[str, object]) -> dict[str, object]:
|
||||
changed = deepcopy(payload)
|
||||
job = changed["job"]
|
||||
assert isinstance(job, dict)
|
||||
job["command"] = ["python", "untrusted.py"]
|
||||
return changed
|
||||
|
||||
|
||||
def _corrupt_job_identity(payload: dict[str, object]) -> dict[str, object]:
|
||||
changed = deepcopy(payload)
|
||||
job = changed["job"]
|
||||
assert isinstance(job, dict)
|
||||
source = job["source"]
|
||||
assert isinstance(source, dict)
|
||||
source["bundle_sha256"] = "c" * 64
|
||||
return changed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mutator",
|
||||
[_spoof_claimant, _inject_unknown_execution_payload, _corrupt_job_identity],
|
||||
)
|
||||
def test_spoofed_unknown_or_corrupted_claim_is_rejected_without_execution(
|
||||
tmp_path: Path,
|
||||
mutator: ClaimMutator,
|
||||
) -> None:
|
||||
queue = _queue(tmp_path)
|
||||
job_id = _enqueue(queue)
|
||||
transport = FakeTransport(queue, claim_mutator=mutator)
|
||||
executor = FakeExecutor()
|
||||
|
||||
report = _agent(transport, executor).run_once()
|
||||
|
||||
assert report.state == "rejected"
|
||||
assert report.failure_code == "claim-rejected"
|
||||
assert transport.starts == []
|
||||
assert transport.successes == []
|
||||
assert transport.failures == []
|
||||
assert executor.jobs == []
|
||||
assert queue.get(job_id).state == "claimed"
|
||||
|
||||
|
||||
def test_executor_error_is_reported_as_bounded_failure(tmp_path: Path) -> None:
|
||||
queue = _queue(tmp_path)
|
||||
job_id = _enqueue(queue)
|
||||
transport = FakeTransport(queue)
|
||||
executor = FakeExecutor(error=RuntimeError("unsafe\n" + ("x" * 2_000)))
|
||||
|
||||
report = _agent(transport, executor).run_once()
|
||||
|
||||
assert report.state == "failed"
|
||||
assert report.failure_code == "executor-error"
|
||||
assert transport.starts == [job_id]
|
||||
assert transport.successes == []
|
||||
assert len(transport.failures) == 1
|
||||
failed_job_id, failure_code, message = transport.failures[0]
|
||||
assert failed_job_id == job_id
|
||||
assert failure_code == "executor-error"
|
||||
assert len(message) <= MAX_EXECUTOR_FAILURE_MESSAGE_LENGTH
|
||||
assert "\n" not in message
|
||||
assert queue.get(job_id).state == "failed"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BlockingEmptyTransport:
|
||||
entered: Event
|
||||
release: Event
|
||||
|
||||
def claim_next(
|
||||
self,
|
||||
*,
|
||||
claimant_id: str,
|
||||
claim_request_id: str,
|
||||
) -> Mapping[str, object] | None:
|
||||
assert claimant_id == WORKER_006_CONTOUR_ID
|
||||
assert claim_request_id == "worker-006:overlap-test"
|
||||
self.entered.set()
|
||||
assert self.release.wait(timeout=2)
|
||||
return None
|
||||
|
||||
def start(
|
||||
self,
|
||||
*,
|
||||
claimant_id: str,
|
||||
job_id: str,
|
||||
claim_token: str,
|
||||
) -> Mapping[str, object]:
|
||||
raise AssertionError("an empty transport cannot start a job")
|
||||
|
||||
def succeed(
|
||||
self,
|
||||
*,
|
||||
claimant_id: str,
|
||||
job_id: str,
|
||||
claim_token: str,
|
||||
result_id: str,
|
||||
result_sha256: str,
|
||||
) -> Mapping[str, object]:
|
||||
raise AssertionError("an empty transport cannot succeed a job")
|
||||
|
||||
def fail(
|
||||
self,
|
||||
*,
|
||||
claimant_id: str,
|
||||
job_id: str,
|
||||
claim_token: str,
|
||||
error_code: str,
|
||||
message: str,
|
||||
) -> Mapping[str, object]:
|
||||
raise AssertionError("an empty transport cannot fail a job")
|
||||
|
||||
|
||||
def test_worker_agent_allows_only_one_claim_cycle_at_a_time() -> None:
|
||||
entered = Event()
|
||||
release = Event()
|
||||
transport = BlockingEmptyTransport(entered=entered, release=release)
|
||||
agent = ObservatoryWorkerAgent(
|
||||
transport=transport,
|
||||
executors=ObservatoryWorkerExecutorRegistry(()),
|
||||
claim_request_id_factory=lambda: "worker-006:overlap-test",
|
||||
)
|
||||
reports: list[object] = []
|
||||
|
||||
first = Thread(target=lambda: reports.append(agent.run_once()))
|
||||
first.start()
|
||||
assert entered.wait(timeout=2)
|
||||
with pytest.raises(ObservatoryWorkerAgentBusyError, match="active claim"):
|
||||
agent.run_once()
|
||||
release.set()
|
||||
first.join(timeout=2)
|
||||
|
||||
assert not first.is_alive()
|
||||
assert len(reports) == 1
|
||||
@@ -0,0 +1,408 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from k1link.observatory.recorded_jobs import (
|
||||
ObservatoryRecordedJobIntent,
|
||||
ObservatoryRecordedJobQueue,
|
||||
RecordedRunDefinition,
|
||||
RecordedRunDefinitionRegistry,
|
||||
)
|
||||
from k1link.web.observatory_worker_api import (
|
||||
OBSERVATORY_WORKER_CONTOUR_HEADER,
|
||||
ObservatoryWorkerAuthentication,
|
||||
build_observatory_worker_router,
|
||||
load_observatory_worker_authentication,
|
||||
)
|
||||
|
||||
WORKER_TOKEN = "worker-006-test-bearer-secret-32bytes"
|
||||
WORKER_TOKEN_SHA256 = hashlib.sha256(WORKER_TOKEN.encode()).hexdigest()
|
||||
WORKER_HEADERS = {
|
||||
"Authorization": f"Bearer {WORKER_TOKEN}",
|
||||
OBSERVATORY_WORKER_CONTOUR_HEADER: "worker-006",
|
||||
}
|
||||
CLAIM_SCHEMA = "missioncore.observatory-worker-claim-request/v1"
|
||||
START_SCHEMA = "missioncore.observatory-worker-start-request/v1"
|
||||
CHECKPOINT_SCHEMA = "missioncore.observatory-worker-checkpoint-request/v1"
|
||||
SUCCEED_SCHEMA = "missioncore.observatory-worker-succeed-request/v1"
|
||||
FAIL_SCHEMA = "missioncore.observatory-worker-fail-request/v1"
|
||||
|
||||
|
||||
def _definition() -> RecordedRunDefinition:
|
||||
return RecordedRunDefinition(
|
||||
setup_id="portable-lab-v1",
|
||||
definition_id="portable-lab-v1-definition",
|
||||
definition_version=1,
|
||||
definition_sha256="1" * 64,
|
||||
source_adapter_id="sealed-session-source",
|
||||
source_adapter_version=1,
|
||||
source_adapter_sha256="2" * 64,
|
||||
executor_release_id="portable-lab-v1-worker",
|
||||
executor_release_sha256="3" * 64,
|
||||
executor_image_sha256="4" * 64,
|
||||
model_release_ids=("eomt-cityscapes-large", "ddrnet-39"),
|
||||
model_manifest_sha256="5" * 64,
|
||||
resource_profile_id="worker006-single-gpu",
|
||||
resource_profile_sha256="6" * 64,
|
||||
checkpoint_policy="cooperative",
|
||||
allowed_checkpoints=("semantic-pass",),
|
||||
)
|
||||
|
||||
|
||||
def _services(tmp_path: Path) -> tuple[TestClient, ObservatoryRecordedJobQueue]:
|
||||
definition = _definition()
|
||||
queue = ObservatoryRecordedJobQueue(
|
||||
tmp_path,
|
||||
definitions=RecordedRunDefinitionRegistry((definition,)),
|
||||
)
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
build_observatory_worker_router(
|
||||
queue,
|
||||
authentication=ObservatoryWorkerAuthentication(
|
||||
bearer_token_sha256=WORKER_TOKEN_SHA256,
|
||||
contour_id="worker-006",
|
||||
),
|
||||
)
|
||||
)
|
||||
return TestClient(app), queue
|
||||
|
||||
|
||||
def _enqueue(
|
||||
queue: ObservatoryRecordedJobQueue,
|
||||
*,
|
||||
idempotency_key: str = "worker-api:test-job",
|
||||
) -> str:
|
||||
definition = _definition()
|
||||
job, created = queue.submit(
|
||||
ObservatoryRecordedJobIntent(
|
||||
idempotency_key=idempotency_key,
|
||||
source_session_id="20260831T120000Z_viewer_live",
|
||||
source_catalog_sha256="7" * 64,
|
||||
source_bundle_sha256="8" * 64,
|
||||
source_capability_manifest_sha256="9" * 64,
|
||||
setup_id=definition.setup_id,
|
||||
definition_sha256=definition.definition_sha256,
|
||||
),
|
||||
enqueue=True,
|
||||
)
|
||||
assert created is True
|
||||
return job.job_id
|
||||
|
||||
|
||||
def _claim(
|
||||
client: TestClient,
|
||||
*,
|
||||
claim_request_id: str = "worker-006:test-claim",
|
||||
) -> dict[str, Any]:
|
||||
response = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers=WORKER_HEADERS,
|
||||
json={
|
||||
"schema_version": CLAIM_SCHEMA,
|
||||
"claim_request_id": claim_request_id,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_worker_authentication_requires_digest_and_configured_contour(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
client, queue = _services(tmp_path)
|
||||
_enqueue(queue)
|
||||
request = {
|
||||
"schema_version": CLAIM_SCHEMA,
|
||||
"claim_request_id": "worker-006:auth-claim",
|
||||
}
|
||||
|
||||
missing = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
json=request,
|
||||
)
|
||||
wrong_token = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers={
|
||||
"Authorization": "Bearer wrong-secret",
|
||||
OBSERVATORY_WORKER_CONTOUR_HEADER: "worker-006",
|
||||
},
|
||||
json=request,
|
||||
)
|
||||
wrong_contour = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers={
|
||||
"Authorization": f"Bearer {WORKER_TOKEN}",
|
||||
OBSERVATORY_WORKER_CONTOUR_HEADER: "worker-007",
|
||||
},
|
||||
json=request,
|
||||
)
|
||||
|
||||
assert missing.status_code == 401
|
||||
assert missing.headers["www-authenticate"] == "Bearer"
|
||||
assert wrong_token.status_code == 401
|
||||
assert wrong_contour.status_code == 403
|
||||
assert queue.list_jobs()[0].state == "queued"
|
||||
|
||||
claimed = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
assert claimed.status_code == 200
|
||||
assert claimed.json()["claimant_id"] == "worker-006"
|
||||
assert WORKER_TOKEN not in claimed.text
|
||||
|
||||
|
||||
def test_claim_is_idempotent_and_request_schema_rejects_execution_inputs(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
client, queue = _services(tmp_path)
|
||||
job_id = _enqueue(queue)
|
||||
request = {
|
||||
"schema_version": CLAIM_SCHEMA,
|
||||
"claim_request_id": "worker-006:stable-claim",
|
||||
}
|
||||
|
||||
first = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
repeated = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
forbidden_inputs = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/start",
|
||||
headers=WORKER_HEADERS,
|
||||
json={
|
||||
"schema_version": START_SCHEMA,
|
||||
"claim_token": first.json()["claim_token"],
|
||||
"command": ["python", "worker.py"],
|
||||
"working_path": "/tmp/run",
|
||||
"environment": {"CUDA_VISIBLE_DEVICES": "0"},
|
||||
"image": "unsealed:latest",
|
||||
},
|
||||
)
|
||||
|
||||
assert first.status_code == 200
|
||||
assert repeated.status_code == 200
|
||||
assert first.json()["job"]["job_id"] == job_id
|
||||
assert repeated.json() == first.json()
|
||||
assert queue.get(job_id).claim_generation == 1
|
||||
assert forbidden_inputs.status_code == 422
|
||||
assert queue.get(job_id).state == "claimed"
|
||||
|
||||
|
||||
def test_empty_claim_is_204_and_empty_receipt_remains_idempotent(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
client, queue = _services(tmp_path)
|
||||
request = {
|
||||
"schema_version": CLAIM_SCHEMA,
|
||||
"claim_request_id": "worker-006:empty-poll",
|
||||
}
|
||||
|
||||
first = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
_enqueue(queue)
|
||||
repeated = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
fresh = client.post(
|
||||
"/api/v1/worker/observatory/recorded-jobs/claims",
|
||||
headers=WORKER_HEADERS,
|
||||
json={
|
||||
"schema_version": CLAIM_SCHEMA,
|
||||
"claim_request_id": "worker-006:fresh-poll",
|
||||
},
|
||||
)
|
||||
|
||||
assert first.status_code == 204
|
||||
assert first.content == b""
|
||||
assert repeated.status_code == 204
|
||||
assert fresh.status_code == 200
|
||||
|
||||
|
||||
def test_worker_can_start_checkpoint_and_read_job_but_stale_token_fails_closed(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
client, queue = _services(tmp_path)
|
||||
job_id = _enqueue(queue)
|
||||
claim = _claim(client)
|
||||
claim_token = claim["claim_token"]
|
||||
|
||||
stale = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/start",
|
||||
headers=WORKER_HEADERS,
|
||||
json={"schema_version": START_SCHEMA, "claim_token": "a" * 64},
|
||||
)
|
||||
started = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/start",
|
||||
headers=WORKER_HEADERS,
|
||||
json={"schema_version": START_SCHEMA, "claim_token": claim_token},
|
||||
)
|
||||
checkpointed = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/checkpoint",
|
||||
headers=WORKER_HEADERS,
|
||||
json={
|
||||
"schema_version": CHECKPOINT_SCHEMA,
|
||||
"claim_token": claim_token,
|
||||
"checkpoint_id": "semantic-pass",
|
||||
},
|
||||
)
|
||||
fetched = client.get(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}",
|
||||
headers=WORKER_HEADERS,
|
||||
)
|
||||
|
||||
assert stale.status_code == 409
|
||||
assert started.status_code == 200
|
||||
assert started.json()["state"] == "running"
|
||||
assert checkpointed.status_code == 200
|
||||
assert checkpointed.json()["state"] == "running"
|
||||
assert checkpointed.json()["checkpoint_policy"]["last_checkpoint_id"] == ("semantic-pass")
|
||||
assert fetched.status_code == 200
|
||||
assert fetched.json()["job_id"] == job_id
|
||||
assert "active_claim_token" not in fetched.json()
|
||||
|
||||
|
||||
def test_worker_can_publish_success_idempotently(tmp_path: Path) -> None:
|
||||
client, queue = _services(tmp_path)
|
||||
job_id = _enqueue(queue)
|
||||
claim_token = _claim(client)["claim_token"]
|
||||
client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/start",
|
||||
headers=WORKER_HEADERS,
|
||||
json={"schema_version": START_SCHEMA, "claim_token": claim_token},
|
||||
)
|
||||
request = {
|
||||
"schema_version": SUCCEED_SCHEMA,
|
||||
"claim_token": claim_token,
|
||||
"result_id": "lab-v1-worker-result-001",
|
||||
"result_sha256": "b" * 64,
|
||||
}
|
||||
|
||||
succeeded = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/succeed",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
repeated = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/succeed",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
conflicting_failure = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/fail",
|
||||
headers=WORKER_HEADERS,
|
||||
json={
|
||||
"schema_version": FAIL_SCHEMA,
|
||||
"claim_token": claim_token,
|
||||
"error_code": "executor-failed",
|
||||
"message": "Must not replace sealed success.",
|
||||
},
|
||||
)
|
||||
|
||||
assert succeeded.status_code == 200
|
||||
assert succeeded.json()["state"] == "succeeded"
|
||||
assert succeeded.json()["result"] == {
|
||||
"result_id": "lab-v1-worker-result-001",
|
||||
"sha256": "b" * 64,
|
||||
}
|
||||
assert repeated.status_code == 200
|
||||
assert repeated.json() == succeeded.json()
|
||||
assert conflicting_failure.status_code == 409
|
||||
|
||||
|
||||
def test_worker_can_fail_claimed_job_idempotently(tmp_path: Path) -> None:
|
||||
client, queue = _services(tmp_path)
|
||||
job_id = _enqueue(queue)
|
||||
claim_token = _claim(client)["claim_token"]
|
||||
request = {
|
||||
"schema_version": FAIL_SCHEMA,
|
||||
"claim_token": claim_token,
|
||||
"error_code": "model-unavailable",
|
||||
"message": "Sealed model release is not installed.",
|
||||
}
|
||||
|
||||
failed = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/fail",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
repeated = client.post(
|
||||
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/fail",
|
||||
headers=WORKER_HEADERS,
|
||||
json=request,
|
||||
)
|
||||
|
||||
assert failed.status_code == 200
|
||||
assert failed.json()["state"] == "failed"
|
||||
assert failed.json()["terminal"] == {
|
||||
"code": "model-unavailable",
|
||||
"message": "Sealed model release is not installed.",
|
||||
}
|
||||
assert repeated.status_code == 200
|
||||
assert repeated.json() == failed.json()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bearer_token_sha256,contour_id",
|
||||
[("not-a-digest", "worker-006"), ("a" * 64, "WORKER 006")],
|
||||
)
|
||||
def test_worker_authentication_configuration_is_strict(
|
||||
bearer_token_sha256: str,
|
||||
contour_id: str,
|
||||
) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ObservatoryWorkerAuthentication(
|
||||
bearer_token_sha256=bearer_token_sha256,
|
||||
contour_id=contour_id,
|
||||
)
|
||||
|
||||
|
||||
def test_worker_authentication_loads_private_regular_token_file(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
token_path = tmp_path / "observatory-worker.token"
|
||||
token_path.write_text(WORKER_TOKEN, encoding="ascii")
|
||||
token_path.chmod(0o600)
|
||||
|
||||
authentication = load_observatory_worker_authentication(token_path)
|
||||
|
||||
assert authentication == ObservatoryWorkerAuthentication(
|
||||
bearer_token_sha256=WORKER_TOKEN_SHA256,
|
||||
contour_id="worker-006",
|
||||
)
|
||||
assert WORKER_TOKEN not in repr(authentication)
|
||||
|
||||
|
||||
def test_worker_authentication_rejects_broad_permissions_and_symlinks(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
token_path = tmp_path / "observatory-worker.token"
|
||||
token_path.write_text(WORKER_TOKEN, encoding="ascii")
|
||||
token_path.chmod(0o644)
|
||||
|
||||
with pytest.raises(ValueError, match="permissions"):
|
||||
load_observatory_worker_authentication(token_path)
|
||||
|
||||
token_path.chmod(0o600)
|
||||
link = tmp_path / "linked.token"
|
||||
link.symlink_to(token_path)
|
||||
with pytest.raises(ValueError, match="credential"):
|
||||
load_observatory_worker_authentication(link)
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.web import app as app_module
|
||||
|
||||
WORKER_ROUTE_PREFIX = "/api/v1/worker/observatory"
|
||||
|
||||
|
||||
def test_worker_router_is_hard_disabled_until_lease_and_publisher_exist() -> None:
|
||||
assert app_module.OBSERVATORY_RECORDED_JOB_QUEUE is not None
|
||||
assert app_module.OBSERVATORY_WORKER_CLAIM_LEASE_READY is False
|
||||
assert app_module.OBSERVATORY_WORKER_VERIFIED_RESULT_PUBLISHER_READY is False
|
||||
assert app_module.OBSERVATORY_WORKER_PRODUCTION_API_ENABLED is False
|
||||
assert app_module.OBSERVATORY_WORKER_AUTHENTICATION is None
|
||||
assert app_module.OBSERVATORY_WORKER_API_ERROR is not None
|
||||
assert "hard-disabled" in app_module.OBSERVATORY_WORKER_API_ERROR
|
||||
assert not any(
|
||||
getattr(route, "path", "").startswith(WORKER_ROUTE_PREFIX)
|
||||
for route in app_module.app.routes
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_worker_credential_disables_only_optional_authentication(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
token_path = tmp_path / "observatory-worker.token"
|
||||
token_path.write_text("worker-006-test-bearer-secret-32bytes", encoding="ascii")
|
||||
token_path.chmod(0o644)
|
||||
|
||||
authentication, error = app_module._load_optional_observatory_worker_authentication(
|
||||
app_module.OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
token_path=token_path,
|
||||
)
|
||||
|
||||
assert authentication is None
|
||||
assert error is not None and "permissions" in error
|
||||
|
||||
|
||||
def test_valid_worker_credential_cannot_enable_production_router(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
token_path = tmp_path / "observatory-worker.token"
|
||||
token_path.write_text("worker-006-test-bearer-secret-32bytes", encoding="ascii")
|
||||
token_path.chmod(0o600)
|
||||
|
||||
authentication, error = app_module._load_optional_observatory_worker_authentication(
|
||||
app_module.OBSERVATORY_RECORDED_JOB_QUEUE,
|
||||
token_path=token_path,
|
||||
)
|
||||
|
||||
assert authentication is not None
|
||||
assert error is None
|
||||
assert app_module.OBSERVATORY_WORKER_PRODUCTION_API_ENABLED is False
|
||||
assert app_module.OBSERVATORY_WORKER_AUTHENTICATION is None
|
||||
assert not any(
|
||||
getattr(route, "path", "").startswith(WORKER_ROUTE_PREFIX)
|
||||
for route in app_module.app.routes
|
||||
)
|
||||
Reference in New Issue
Block a user