feat(observatory): admit canonical recorded replay
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
import k1link.laboratory.canonical_recorded_catalog as canonical_catalog
|
||||
from k1link.laboratory.canonical_recorded_catalog import (
|
||||
CANONICAL_REPLAY_CAPABILITY_SCHEMA,
|
||||
CanonicalRecordedCatalogError,
|
||||
publish_canonical_recorded_vegetation_result,
|
||||
)
|
||||
|
||||
|
||||
class CapturingStore:
|
||||
def __init__(self, *, source_label: str = "RAVNOVES004TREE") -> None:
|
||||
self.parameters: dict[str, Any] | None = None
|
||||
self.source_label = source_label
|
||||
|
||||
def publish_lab_instance(self, **parameters: Any) -> dict[str, Any]:
|
||||
self.parameters = parameters
|
||||
return parameters
|
||||
|
||||
def get_session_with_catalog_snapshot(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> tuple[SimpleNamespace, str]:
|
||||
assert session_id == "20260828T130511Z_viewer_live"
|
||||
summary = SimpleNamespace(
|
||||
session_id=session_id,
|
||||
display_name=self.source_label,
|
||||
status="ready",
|
||||
started_at_utc="2026-08-28T13:05:16.249Z",
|
||||
completed_at_utc="2026-08-28T13:18:45.030Z",
|
||||
duration_seconds=808.779495667,
|
||||
modalities=("point-cloud", "trajectory", "video"),
|
||||
source_count=3,
|
||||
total_bytes=799_020_963,
|
||||
replayable=True,
|
||||
lab=None,
|
||||
)
|
||||
sources = tuple(
|
||||
SimpleNamespace(
|
||||
source_id=source_id,
|
||||
semantic_channel_id=semantic_channel_id,
|
||||
modality=modality,
|
||||
status="recorded",
|
||||
seekable=True,
|
||||
artifact_id=artifact_id,
|
||||
)
|
||||
for source_id, semantic_channel_id, modality, artifact_id in (
|
||||
(
|
||||
"sensor.camera.right",
|
||||
"camera.video.recorded",
|
||||
"video",
|
||||
"recorded-video-6a3945242828a038",
|
||||
),
|
||||
(
|
||||
"sensor.lidar.primary",
|
||||
"spatial.point-cloud.recorded",
|
||||
"point-cloud",
|
||||
"raw-transport-primary",
|
||||
),
|
||||
(
|
||||
"spatial.trajectory",
|
||||
"spatial.pose.recorded",
|
||||
"trajectory",
|
||||
"raw-transport-primary",
|
||||
),
|
||||
)
|
||||
)
|
||||
artifacts = (
|
||||
SimpleNamespace(
|
||||
artifact_id="raw-transport-clock",
|
||||
kind="raw-transport-clock",
|
||||
media_type="application/json",
|
||||
byte_length=189,
|
||||
sha256="1" * 64,
|
||||
integrity_status="verified",
|
||||
),
|
||||
SimpleNamespace(
|
||||
artifact_id="raw-transport-clock-origin",
|
||||
kind="raw-transport-clock-origin",
|
||||
media_type="application/json",
|
||||
byte_length=103,
|
||||
sha256="2" * 64,
|
||||
integrity_status="verified",
|
||||
),
|
||||
SimpleNamespace(
|
||||
artifact_id="raw-transport-index",
|
||||
kind="raw-transport-index",
|
||||
media_type="application/x-ndjson",
|
||||
byte_length=7_679_275,
|
||||
sha256=None,
|
||||
integrity_status="verified",
|
||||
),
|
||||
SimpleNamespace(
|
||||
artifact_id="raw-transport-primary",
|
||||
kind="raw-transport",
|
||||
media_type="application/x-nodedc-k1mqtt",
|
||||
byte_length=245_183_013,
|
||||
sha256="20c789eff922a6bbb53592f86614abc0729a30544df29e740e7a378d12af85c2",
|
||||
integrity_status="verified",
|
||||
),
|
||||
SimpleNamespace(
|
||||
artifact_id="recorded-video-6a3945242828a038",
|
||||
kind="recorded-video",
|
||||
media_type="video/mp4",
|
||||
byte_length=553_837_950,
|
||||
sha256=None,
|
||||
integrity_status="validated-structure",
|
||||
),
|
||||
)
|
||||
return (
|
||||
SimpleNamespace(
|
||||
summary=summary,
|
||||
sources=sources,
|
||||
artifacts=artifacts,
|
||||
plugin_id="nodedc.device.xgrids-lixelkity-k1",
|
||||
archive_id="xgrids-k1.viewer-live.evidence",
|
||||
),
|
||||
"a" * 64,
|
||||
)
|
||||
|
||||
|
||||
def _canonical_sha256(value: object) -> str:
|
||||
encoded = json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def _sealed_result(runtime_root: Path) -> Path:
|
||||
authority = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
"camera_semantics_can_clear_rigid_geometry": False,
|
||||
}
|
||||
timeline_payload = struct.pack(
|
||||
"<6830Q",
|
||||
*(39_250_000_000 + index * 100_000_000 for index in range(6830)),
|
||||
)
|
||||
artifact_payloads = {
|
||||
"video/eomt-semantic-masks.zip": b"fixture-city-mask-archive",
|
||||
"video/ddrnet-semantic-masks.zip": b"fixture-vegetation-mask-archive",
|
||||
"video/frame-source-times-ns.bin": timeline_payload,
|
||||
"proofs/job.json": b'{"fixture":"job"}',
|
||||
"proofs/decode_repair.json": b'{"fixture":"decode"}',
|
||||
"proofs/ddrnet_decode_repair.json": b'{"fixture":"decode"}',
|
||||
}
|
||||
|
||||
def proof(path: str) -> dict[str, object]:
|
||||
payload = artifact_payloads[path]
|
||||
return {
|
||||
"path": path,
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"byte_length": len(payload),
|
||||
}
|
||||
|
||||
def taxonomy(schema: str, count: int) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": schema,
|
||||
"classes": [
|
||||
{
|
||||
"class_id": index,
|
||||
"label": f"class-{index}",
|
||||
"color_rgb": [index % 256, (index * 2) % 256, (index * 3) % 256],
|
||||
"disposition": "undefined" if index == 0 else "prediction",
|
||||
}
|
||||
for index in range(count)
|
||||
],
|
||||
}
|
||||
|
||||
review = {
|
||||
"source_id": "RAVNOVES004TREE",
|
||||
"session_id": "20260828T130511Z_viewer_live",
|
||||
"source_job_id": "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
"source_job_input_sha256": (
|
||||
"eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715"
|
||||
),
|
||||
"source_stream_sha256": (
|
||||
"e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac"
|
||||
),
|
||||
"recorded_media_source_id": "recorded.camera.6a3945242828a038",
|
||||
"recorded_media_generation_sha256": (
|
||||
"b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded"
|
||||
),
|
||||
"linked_route_review_result_id": (
|
||||
"lab-v1-vegetation-shadow-" + "9" * 64
|
||||
),
|
||||
"frame_count": 6830,
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"ground_truth": False,
|
||||
"timeline_start_seconds": 39.25,
|
||||
"timeline_end_seconds": 757.25,
|
||||
"timeline": {
|
||||
**proof("video/frame-source-times-ns.bin"),
|
||||
"encoding": "uint64-le-nanoseconds",
|
||||
"frame_count": 6830,
|
||||
},
|
||||
"decode_repair": {
|
||||
"repaired_frame_count": 1,
|
||||
"sequence": 6092,
|
||||
"method": "duplicate-previous-decoded-frame",
|
||||
"proofs": {
|
||||
"eomt": proof("proofs/decode_repair.json"),
|
||||
"ddrnet": proof("proofs/ddrnet_decode_repair.json"),
|
||||
},
|
||||
},
|
||||
"proofs": {"job": proof("proofs/job.json")},
|
||||
"layers": {
|
||||
"city": {
|
||||
"name": "EoMT Cityscapes",
|
||||
"result_id": "result-" + "1" * 64,
|
||||
"frame_count": 6830,
|
||||
"taxonomy": taxonomy(
|
||||
"missioncore.recorded-eomt-taxonomy/v1",
|
||||
16,
|
||||
),
|
||||
"mask_archive": proof("video/eomt-semantic-masks.zip"),
|
||||
"inference_fps": 3.0,
|
||||
"latency_p95_ms": 361.0,
|
||||
"peak_reserved_vram_bytes": 2_977_955_840,
|
||||
},
|
||||
"vegetation": {
|
||||
"name": "ddrnet_39",
|
||||
"result_id": "lab-v1-ravnoves-video-ddrnet-" + "2" * 64,
|
||||
"frame_count": 6830,
|
||||
"taxonomy": taxonomy(
|
||||
"missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||
64,
|
||||
),
|
||||
"mask_archive": proof("video/ddrnet-semantic-masks.zip"),
|
||||
"inference_fps": 52.0,
|
||||
"latency_p95_ms": 27.0,
|
||||
"peak_reserved_vram_bytes": 331_350_016,
|
||||
},
|
||||
},
|
||||
}
|
||||
candidates = {
|
||||
candidate: {
|
||||
"loaded_model_name": candidate,
|
||||
"checkpoint_sha256": str(index) * 64,
|
||||
"validation_metrics": {
|
||||
"mean_iou_percent": 70.0,
|
||||
"published_mean_iou_percent": 69.0,
|
||||
"vegetation_mean_iou": 0.7,
|
||||
},
|
||||
"validation_timing": {
|
||||
"latency_ms_p95": 20.0,
|
||||
"throughput_fps_from_mean_inference": 50.0,
|
||||
},
|
||||
"shadow_timing": {
|
||||
"latency_ms_p95": 21.0,
|
||||
"throughput_fps_from_mean_inference": 49.0,
|
||||
"prewarm_latency_ms": 100.0,
|
||||
},
|
||||
"resource": {
|
||||
"peak_reserved_vram_bytes": 1024,
|
||||
"gpu_name": "fixture-gpu",
|
||||
},
|
||||
}
|
||||
for index, candidate in enumerate(("ddrnet", "ppliteseg"), start=3)
|
||||
}
|
||||
source = {
|
||||
"shadow_session": "RAVNOVES004TREE",
|
||||
"shadow_camera": "sensor.camera.right",
|
||||
"shadow_frame_count": 6830,
|
||||
"video_shadow_frame_count": 6830,
|
||||
}
|
||||
identity = {
|
||||
"authority": authority,
|
||||
"base_result_id": "lab-v1-vegetation-shadow-" + "9" * 64,
|
||||
"route_full_review": review,
|
||||
"selected_candidate": "ddrnet",
|
||||
"candidate_metrics": candidates,
|
||||
"source": source,
|
||||
}
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
result_id = f"lab-v1-vegetation-shadow-{identity_sha256}"
|
||||
result_root = runtime_root / "lab-v1-vegetation" / "results" / result_id
|
||||
result_root.mkdir(parents=True)
|
||||
document = {
|
||||
"schema_version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"result_id": result_id,
|
||||
"identity": identity,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": "2026-08-29T18:05:11.329061+00:00",
|
||||
"ground_truth": False,
|
||||
"status": "visual-shadow-ready-policy-not-authorized",
|
||||
"authority": dict(authority),
|
||||
"source": source,
|
||||
"route_video": None,
|
||||
"route_review": None,
|
||||
"route_full_review": dict(review),
|
||||
"method": {
|
||||
"completeness": "complete",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
},
|
||||
"metrics": {"candidates": candidates},
|
||||
"decision": {
|
||||
"selected_candidate": "ddrnet",
|
||||
"visual_shadow_ready": True,
|
||||
"mission_policy_ready_for_configuration": True,
|
||||
"navigation_accepted": False,
|
||||
"production_accepted": False,
|
||||
},
|
||||
"limitations": ["fixture is observation-only"],
|
||||
"catalogs": {"goose": [], "ravnoves": []},
|
||||
"artifacts": [
|
||||
{
|
||||
**proof(path),
|
||||
"role": "fixture",
|
||||
"media_type": (
|
||||
"application/zip"
|
||||
if path.endswith(".zip")
|
||||
else "application/octet-stream"
|
||||
if path.endswith(".bin")
|
||||
else "application/json"
|
||||
),
|
||||
}
|
||||
for path in artifact_payloads
|
||||
],
|
||||
}
|
||||
for relative, payload in artifact_payloads.items():
|
||||
path = result_root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(payload)
|
||||
(result_root / "result.json").write_text(
|
||||
json.dumps(document, ensure_ascii=False, sort_keys=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result_root
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_uses_exact_session_binding_without_compute(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
store = CapturingStore()
|
||||
|
||||
published = publish_canonical_recorded_vegetation_result(
|
||||
store=store, # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
assert published["session_id"] == result_root.name
|
||||
assert published["source_session_id"] == "20260828T130511Z_viewer_live"
|
||||
assert published["source_result_id"] == "lab-v1-vegetation-shadow-" + "9" * 64
|
||||
assert published["config_sha256"] is None
|
||||
assert published["duration_seconds"] == pytest.approx(718.0)
|
||||
assert published["include_recorded_media"] is False
|
||||
assert published["expected_source_catalog_sha256"] == "a" * 64
|
||||
capability = published["replay_capability"]
|
||||
assert capability.as_dict() == {
|
||||
"schema_version": CANONICAL_REPLAY_CAPABILITY_SCHEMA,
|
||||
"kind": "canonical-recorded-rerun",
|
||||
"viewer_profile": "recorded-session",
|
||||
"timeline": "session_time",
|
||||
"activation": "explicit",
|
||||
"commands_enabled": False,
|
||||
}
|
||||
assert published["provenance"]["replay_capability"] == capability.as_dict()
|
||||
assert published["provenance"]["method"]["completeness"] == "legacy-partial"
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_authority_and_unregistered_root(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
document_path = result_root / "result.json"
|
||||
document = json.loads(document_path.read_text(encoding="utf-8"))
|
||||
document["authority"]["commands_enabled"] = True
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="control authority"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
document["authority"]["commands_enabled"] = False
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
outside = tmp_path / result_root.name
|
||||
outside.mkdir()
|
||||
(outside / "result.json").write_text(
|
||||
document_path.read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="registered root"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=outside,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_non_boolean_authority(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
document_path = result_root / "result.json"
|
||||
document = json.loads(document_path.read_text(encoding="utf-8"))
|
||||
document["authority"]["commands_enabled"] = 0
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="control authority"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_viewer_incomplete_result(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
document_path = result_root / "result.json"
|
||||
document = json.loads(document_path.read_text(encoding="utf-8"))
|
||||
document.pop("metrics")
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
store = CapturingStore()
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="result metrics"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=store, # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
assert store.parameters is None
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_unbound_display_metrics(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
document_path = result_root / "result.json"
|
||||
document = json.loads(document_path.read_text(encoding="utf-8"))
|
||||
document["metrics"]["candidates"]["ddrnet"]["loaded_model_name"] = "tampered"
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="identity-bound"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_wrong_source_catalog_identity(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="source catalog identity"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(source_label="different source"), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_intermediate_symlink_escape(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
outside_runtime = tmp_path / "outside-runtime"
|
||||
outside_runtime.mkdir()
|
||||
actual_result = _sealed_result(outside_runtime)
|
||||
(runtime_root / "lab-v1-vegetation").symlink_to(
|
||||
outside_runtime / "lab-v1-vegetation",
|
||||
target_is_directory=True,
|
||||
)
|
||||
escaped_result = (
|
||||
runtime_root
|
||||
/ "lab-v1-vegetation"
|
||||
/ "results"
|
||||
/ actual_result.name
|
||||
)
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="runtime root"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=escaped_result,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_document_changed_after_proof(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
original = canonical_catalog.verify_laboratory_evidence_result
|
||||
|
||||
def mutate_after_verification(*args: object, **kwargs: object) -> dict[str, object]:
|
||||
proof = original(*args, **kwargs) # type: ignore[arg-type]
|
||||
path = result_root / "result.json"
|
||||
document = json.loads(path.read_text(encoding="utf-8"))
|
||||
document["method"]["pipeline_id"] = "tampered-after-proof/v1"
|
||||
path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
return proof
|
||||
|
||||
monkeypatch.setattr(
|
||||
canonical_catalog,
|
||||
"verify_laboratory_evidence_result",
|
||||
mutate_after_verification,
|
||||
)
|
||||
store = CapturingStore()
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="changed after verification"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=store, # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
assert store.parameters is None
|
||||
@@ -12,9 +12,10 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import k1link.sessions.media as recorded_media_module
|
||||
import k1link.web.session_api as session_api_module
|
||||
@@ -22,6 +23,7 @@ from k1link.compute import RecordedPerceptionOverlayArtifact, RecordedPerception
|
||||
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
|
||||
from k1link.sessions import (
|
||||
LabReplayCapability,
|
||||
MaterializedRecording,
|
||||
RecordedMediaInspector,
|
||||
RecordedMediaManifest,
|
||||
@@ -347,6 +349,79 @@ def test_session_router_exposes_immutable_lab_provenance(tmp_path: Path) -> None
|
||||
assert_no_local_paths((item, detail), repository)
|
||||
|
||||
|
||||
def test_session_router_rolls_capability_projections_out_only_in_v2(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
legacy = store.publish_lab_instance(
|
||||
session_id="lab-e21-legacy",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB E21 · legacy",
|
||||
lab_id="LAB E21",
|
||||
result_kind="e21-realtime-envelope",
|
||||
result_id="e21-realtime-envelope-" + "1" * 64,
|
||||
run_created_at_utc="2026-07-23T15:55:15.548Z",
|
||||
provenance={"method": lab_method()},
|
||||
)
|
||||
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,
|
||||
)
|
||||
canonical = store.publish_lab_instance(
|
||||
session_id="lab-v1-vegetation-shadow-" + "8" * 64,
|
||||
source_session_id=source.name,
|
||||
display_name="RAV004 · recorded",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-vegetation-shadow-" + "8" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"replay_capability": capability.as_dict(),
|
||||
"method": lab_method(),
|
||||
},
|
||||
)
|
||||
router = build_session_router(store)
|
||||
list_route = endpoint(router, "/api/v1/observation-sessions", "GET")
|
||||
|
||||
default_items = list_route(limit=20, cursor=None, scope="all")["items"]
|
||||
assert {item["id"] for item in default_items} == {source.name, legacy.session_id}
|
||||
default_legacy = next(item for item in default_items if item["id"] == legacy.session_id)
|
||||
assert "replay_capability" not in default_legacy["lab"]
|
||||
|
||||
v1_labs = list_route(
|
||||
limit=20,
|
||||
cursor=None,
|
||||
scope="laboratory",
|
||||
lab_contract="v1",
|
||||
)["items"]
|
||||
assert [item["id"] for item in v1_labs] == [legacy.session_id]
|
||||
v2_labs = list_route(
|
||||
limit=20,
|
||||
cursor=None,
|
||||
scope="laboratory",
|
||||
lab_contract="v2",
|
||||
)["items"]
|
||||
by_id = {item["id"]: item for item in v2_labs}
|
||||
assert set(by_id) == {legacy.session_id, canonical.session_id}
|
||||
assert by_id[legacy.session_id]["lab"]["replay_capability"] is None
|
||||
assert by_id[canonical.session_id]["lab"]["replay_capability"] == capability.as_dict()
|
||||
|
||||
application = FastAPI()
|
||||
application.include_router(router)
|
||||
assert TestClient(application).get(
|
||||
"/api/v1/observation-sessions?lab_contract=v3"
|
||||
).status_code == 422
|
||||
|
||||
|
||||
def test_delete_session_removes_evidence_and_cache_but_refuses_an_open_recording(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -16,6 +16,7 @@ from fastapi.routing import APIRoute
|
||||
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
|
||||
from k1link.sessions import (
|
||||
LabReplayCapability,
|
||||
SessionRecordingMaterializer,
|
||||
SessionRecordingPreparationManager,
|
||||
SessionStore,
|
||||
@@ -200,6 +201,60 @@ def test_startup_scan_baselines_historical_sessions_without_enqueuing(
|
||||
manager.close()
|
||||
|
||||
|
||||
def test_background_scan_never_prepares_explicit_capability_projection(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = _make_completed_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
monkeypatch.setattr(app_module, "session_store", store)
|
||||
baseline = set(app_module.finalized_replayable_recording_ids())
|
||||
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,
|
||||
)
|
||||
projection = store.publish_lab_instance(
|
||||
session_id="lab-v1-vegetation-shadow-" + "8" * 64,
|
||||
source_session_id=source.name,
|
||||
display_name="RAV004 · explicit recorded review",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-vegetation-shadow-" + "8" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"replay_capability": capability.as_dict(),
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "fixture/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "fixture",
|
||||
"version": "v1",
|
||||
"role": "test",
|
||||
"identity_sha256": "9" * 64,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
current = set(app_module.finalized_replayable_recording_ids())
|
||||
assert baseline == {source.name}
|
||||
assert projection.session_id not in current
|
||||
assert app_module.newly_finalized_recording_ids(baseline, current) == ()
|
||||
|
||||
|
||||
def test_archive_revision_tracks_session_lifecycle_without_capture_churn(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -15,6 +15,7 @@ from k1link.device_plugins.xgrids_k1.mqtt.capture import (
|
||||
iter_capture_frames,
|
||||
)
|
||||
from k1link.sessions import (
|
||||
LabReplayCapability,
|
||||
LayoutConflictError,
|
||||
SessionIntegrityError,
|
||||
SessionNotFoundError,
|
||||
@@ -823,6 +824,7 @@ def test_lab_instance_is_independent_and_never_deletes_source_evidence(
|
||||
"method": lab_method(),
|
||||
},
|
||||
)
|
||||
assert binding.replay_capability is None
|
||||
|
||||
detail = store.get_session(binding.session_id)
|
||||
lab_command = store.prepare_replay(binding.session_id)
|
||||
@@ -879,6 +881,421 @@ def test_lab_instance_publication_is_idempotent_but_provenance_is_immutable(
|
||||
store.publish_lab_instance(**{**parameters, "config_sha256": "0" * 64})
|
||||
|
||||
|
||||
def test_lab_replay_capability_column_migrates_existing_catalog(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
data_dir = tmp_path / "data"
|
||||
initial = SessionStore(repository, data_dir=data_dir)
|
||||
initial.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
legacy = initial.publish_lab_instance(
|
||||
session_id="lab-e19-pre-capability",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB E19 · pre-capability",
|
||||
lab_id="LAB E19",
|
||||
result_kind="e19-legacy",
|
||||
result_id="e19-legacy-result",
|
||||
run_created_at_utc="2026-07-23T05:19:43.138Z",
|
||||
provenance={"method": lab_method()},
|
||||
)
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN replay_capability_json"
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN include_recorded_media"
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
migrated = SessionStore(repository, data_dir=data_dir)
|
||||
with sqlite3.connect(migrated.database_path) as connection:
|
||||
columns = {
|
||||
row[1]
|
||||
for row in connection.execute(
|
||||
"PRAGMA table_info(observation_lab_instances)"
|
||||
)
|
||||
}
|
||||
assert "replay_capability_json" in columns
|
||||
assert "include_recorded_media" in columns
|
||||
assert migrated.get_lab_instance(legacy.session_id).replay_capability is None
|
||||
|
||||
|
||||
def test_migration_types_only_the_exact_rolling_canonical_projection(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260828T130511Z_viewer_live")
|
||||
make_recorded_camera_source(source)
|
||||
data_dir = tmp_path / "data"
|
||||
initial = SessionStore(repository, data_dir=data_dir)
|
||||
initial.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET archive_id = ? WHERE session_id = ?",
|
||||
("xgrids-k1.viewer-live.evidence", source.name),
|
||||
)
|
||||
connection.commit()
|
||||
evidence_identity = "a" * 64
|
||||
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 = {
|
||||
"schema_version": "missioncore.canonical-recorded-lab-projection/v1",
|
||||
"evidence_identity_sha256": evidence_identity,
|
||||
"result_document_sha256": "b" * 64,
|
||||
"replay_capability": capability.as_dict(),
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
},
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "sealed full-route LAB result",
|
||||
"version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"role": "immutable Session catalog projection",
|
||||
"identity_sha256": evidence_identity,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
parameters = {
|
||||
"session_id": f"lab-v1-vegetation-shadow-{evidence_identity}",
|
||||
"source_session_id": source.name,
|
||||
"display_name": "RAVNOVES004TREE · полный маршрут восприятия",
|
||||
"lab_id": "LAB V1",
|
||||
"result_kind": "recorded-perception-qualification",
|
||||
"result_id": f"lab-v1-vegetation-shadow-{evidence_identity}",
|
||||
"source_result_id": "lab-v1-vegetation-shadow-" + "c" * 64,
|
||||
"config_sha256": None,
|
||||
"run_created_at_utc": "2026-08-29T18:05:11.329061+00:00",
|
||||
"duration_seconds": 718.0,
|
||||
"replay_capability": capability,
|
||||
"provenance": provenance,
|
||||
"include_recorded_media": False,
|
||||
}
|
||||
binding = initial.publish_lab_instance(**parameters)
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET modalities_json = ?, source_count = ?, "
|
||||
"total_bytes = ? WHERE session_id = ?",
|
||||
('["point-cloud","trajectory","video"]', 3, 999_999_999, binding.session_id),
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN replay_capability_json"
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN include_recorded_media"
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
migrated = SessionStore(repository, data_dir=data_dir)
|
||||
|
||||
assert migrated.get_lab_instance(binding.session_id).replay_capability == capability
|
||||
migrated_detail = migrated.get_session(binding.session_id)
|
||||
assert migrated_detail.summary.modalities == ("point-cloud", "trajectory")
|
||||
assert migrated_detail.summary.source_count == 2
|
||||
assert all(source.modality != "video" for source in migrated_detail.sources)
|
||||
assert migrated.list_recent(
|
||||
scope="laboratory",
|
||||
include_capability_projections=False,
|
||||
).items == ()
|
||||
assert [
|
||||
item.session_id
|
||||
for item in migrated.list_recent(
|
||||
scope="laboratory",
|
||||
include_capability_projections=True,
|
||||
).items
|
||||
] == [binding.session_id]
|
||||
assert migrated.publish_lab_instance(**parameters).session_id == binding.session_id
|
||||
|
||||
|
||||
def test_migration_rejects_a_non_replayable_canonical_projection(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260828T130511Z_viewer_live")
|
||||
make_recorded_camera_source(source)
|
||||
data_dir = tmp_path / "data"
|
||||
initial = SessionStore(repository, data_dir=data_dir)
|
||||
initial.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET archive_id = ? WHERE session_id = ?",
|
||||
("xgrids-k1.viewer-live.evidence", source.name),
|
||||
)
|
||||
connection.commit()
|
||||
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,
|
||||
)
|
||||
evidence_identity = "a" * 64
|
||||
result_id = f"lab-v1-vegetation-shadow-{evidence_identity}"
|
||||
initial.publish_lab_instance(
|
||||
session_id=result_id,
|
||||
source_session_id=source.name,
|
||||
display_name="RAVNOVES004TREE · полный маршрут восприятия",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id=result_id,
|
||||
source_result_id="lab-v1-vegetation-shadow-" + "c" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
duration_seconds=718.0,
|
||||
include_recorded_media=False,
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"schema_version": "missioncore.canonical-recorded-lab-projection/v1",
|
||||
"evidence_identity_sha256": evidence_identity,
|
||||
"result_document_sha256": "b" * 64,
|
||||
"replay_capability": capability.as_dict(),
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
},
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "sealed full-route LAB result",
|
||||
"version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"role": "immutable Session catalog projection",
|
||||
"identity_sha256": evidence_identity,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET replayable = 0, "
|
||||
"primary_replay_artifact_id = NULL, timeline_origin_epoch_ns = NULL, "
|
||||
"timeline_origin_monotonic_ns = NULL WHERE session_id = ?",
|
||||
(result_id,),
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN replay_capability_json"
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN include_recorded_media"
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="LAB"):
|
||||
SessionStore(repository, data_dir=data_dir)
|
||||
|
||||
|
||||
def test_migration_rejects_boolean_aliases_in_rolling_authority(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
data_dir = tmp_path / "data"
|
||||
store = SessionStore(repository, data_dir=data_dir)
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
capability = {
|
||||
"schema_version": "missioncore.observation-lab-replay-capability/v1",
|
||||
"kind": "canonical-recorded-rerun",
|
||||
"viewer_profile": "recorded-session",
|
||||
"timeline": "session_time",
|
||||
"activation": "explicit",
|
||||
"commands_enabled": False,
|
||||
}
|
||||
evidence_identity = "a" * 64
|
||||
provenance = {
|
||||
"schema_version": "missioncore.canonical-recorded-lab-projection/v1",
|
||||
"evidence_identity_sha256": evidence_identity,
|
||||
"result_document_sha256": "b" * 64,
|
||||
"replay_capability": capability,
|
||||
"authority": {
|
||||
"commands_enabled": 0,
|
||||
"navigation_or_safety_accepted": 0,
|
||||
"actuation_accepted": 0,
|
||||
},
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "sealed full-route LAB result",
|
||||
"version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"role": "immutable Session catalog projection",
|
||||
"identity_sha256": evidence_identity,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
result_id = f"lab-v1-vegetation-shadow-{evidence_identity}"
|
||||
connection.execute("PRAGMA foreign_keys = OFF")
|
||||
connection.execute(
|
||||
"INSERT INTO observation_sessions "
|
||||
"(session_id, plugin_id, archive_id, display_name, status, modalities_json, "
|
||||
"replayable, origin, source_count, total_bytes, allowed_root, session_root, "
|
||||
"created_at_utc, updated_at_utc) "
|
||||
"VALUES (?, ?, ?, ?, 'ready', '[]', 0, ?, 0, 0, ?, ?, ?, ?)",
|
||||
(
|
||||
result_id,
|
||||
"fixture.plugin",
|
||||
"missioncore.lab-instances",
|
||||
"invalid rolling authority",
|
||||
"missioncore.lab-instance/v1",
|
||||
str(repository),
|
||||
str(repository),
|
||||
"2026-08-30T00:00:00Z",
|
||||
"2026-08-30T00:00:00Z",
|
||||
),
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT INTO observation_lab_instances "
|
||||
"(session_id, source_session_id, lab_id, result_kind, result_id, "
|
||||
"source_result_id, config_sha256, run_created_at_utc, published_at_utc, "
|
||||
"include_recorded_media, replay_capability_json, provenance_json) "
|
||||
"VALUES (?, ?, 'LAB V1', 'recorded-perception-qualification', ?, ?, "
|
||||
"NULL, ?, ?, NULL, NULL, ?)",
|
||||
(
|
||||
result_id,
|
||||
"20260828T130511Z_viewer_live",
|
||||
result_id,
|
||||
"lab-v1-vegetation-shadow-" + "c" * 64,
|
||||
"2026-08-29T18:05:11.329061+00:00",
|
||||
"2026-08-30T00:00:00Z",
|
||||
json.dumps(provenance, sort_keys=True),
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
migrated = SessionStore(repository, data_dir=data_dir)
|
||||
with sqlite3.connect(migrated.database_path) as connection:
|
||||
row = connection.execute(
|
||||
"SELECT replay_capability_json, include_recorded_media "
|
||||
"FROM observation_lab_instances WHERE session_id = ?",
|
||||
("lab-v1-vegetation-shadow-" + "a" * 64,),
|
||||
).fetchone()
|
||||
assert row == (None, None)
|
||||
|
||||
|
||||
def test_lab_instance_persists_typed_explicit_recorded_replay_capability(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
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,
|
||||
)
|
||||
|
||||
binding = store.publish_lab_instance(
|
||||
session_id="lab-recorded-replay",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB V1 · recorded replay",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-vegetation-shadow-" + "a" * 64,
|
||||
source_result_id="lab-v1-vegetation-shadow-" + "b" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"replay_capability": capability.as_dict(),
|
||||
"method": lab_method(),
|
||||
},
|
||||
)
|
||||
|
||||
assert binding.replay_capability == capability
|
||||
assert "replay_capability" not in binding.as_dict()
|
||||
assert binding.as_dict()["provenance"]["replay_capability"] == capability.as_dict()
|
||||
assert store.get_lab_instance(binding.session_id) == binding
|
||||
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_lab_instances SET replay_capability_json = ? "
|
||||
"WHERE session_id = ?",
|
||||
('{"kind":"unknown"}', binding.session_id),
|
||||
)
|
||||
connection.commit()
|
||||
with pytest.raises(SessionIntegrityError, match="replay capability"):
|
||||
store.get_lab_instance(binding.session_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0, 1, None, True])
|
||||
def test_lab_replay_capability_rejects_non_literal_false(value: object) -> None:
|
||||
with pytest.raises(ValueError, match="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=value, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def test_lab_instance_rejects_boolean_alias_in_capability_provenance(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
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,
|
||||
)
|
||||
aliased = {**capability.as_dict(), "commands_enabled": 0}
|
||||
|
||||
with pytest.raises(ValueError, match="exactly match provenance"):
|
||||
store.publish_lab_instance(
|
||||
session_id="lab-recorded-replay-alias",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB V1 · recorded replay alias",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-vegetation-shadow-" + "a" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"replay_capability": aliased,
|
||||
"method": lab_method(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_lab_instance_rejects_publication_without_a_method_manifest(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -933,6 +1350,114 @@ def test_bounded_lab_instance_excludes_unbounded_recorded_media(
|
||||
assert store.prepare_replay(binding.session_id).primary_artifact.path.is_file()
|
||||
|
||||
|
||||
def test_capability_projection_summary_is_exact_and_idempotently_repairable(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
make_recorded_camera_source(source)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
_source_detail, source_snapshot_sha256 = (
|
||||
store.get_session_with_catalog_snapshot(source.name)
|
||||
)
|
||||
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,
|
||||
)
|
||||
parameters = {
|
||||
"session_id": "lab-recorded-bounded",
|
||||
"source_session_id": source.name,
|
||||
"display_name": "LAB V1 · bounded recorded review",
|
||||
"lab_id": "LAB V1",
|
||||
"result_kind": "recorded-perception-qualification",
|
||||
"result_id": "lab-v1-vegetation-shadow-" + "1" * 64,
|
||||
"source_result_id": "lab-v1-vegetation-shadow-" + "2" * 64,
|
||||
"run_created_at_utc": "2026-08-29T18:05:11.329061+00:00",
|
||||
"duration_seconds": 59.962,
|
||||
"include_recorded_media": False,
|
||||
"expected_source_catalog_sha256": source_snapshot_sha256,
|
||||
"replay_capability": capability,
|
||||
"provenance": {
|
||||
"replay_capability": capability.as_dict(),
|
||||
"method": lab_method(),
|
||||
},
|
||||
}
|
||||
binding = store.publish_lab_instance(**parameters)
|
||||
detail = store.get_session(binding.session_id)
|
||||
assert detail.summary.modalities == ("point-cloud", "trajectory")
|
||||
assert detail.summary.source_count == len(detail.sources) == 2
|
||||
referenced_artifacts = {item.artifact_id for item in detail.sources}
|
||||
assert detail.summary.total_bytes == sum(
|
||||
artifact.byte_length
|
||||
for artifact in detail.artifacts
|
||||
if artifact.artifact_id in referenced_artifacts
|
||||
)
|
||||
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET modalities_json = ?, source_count = ?, "
|
||||
"total_bytes = ? WHERE session_id = ?",
|
||||
('["point-cloud","trajectory","video"]', 3, 999_999_999, binding.session_id),
|
||||
)
|
||||
connection.commit()
|
||||
assert store.publish_lab_instance(**parameters) == binding
|
||||
repaired = store.get_session(binding.session_id)
|
||||
assert repaired.summary.modalities == ("point-cloud", "trajectory")
|
||||
assert repaired.summary.source_count == 2
|
||||
assert repaired.summary.total_bytes == detail.summary.total_bytes
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="recorded-media policy"):
|
||||
store.publish_lab_instance(**{**parameters, "include_recorded_media": True})
|
||||
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_session_artifacts SET sha256 = ? "
|
||||
"WHERE session_id = ? AND artifact_id = 'raw-transport-primary'",
|
||||
("0" * 64, binding.session_id),
|
||||
)
|
||||
connection.commit()
|
||||
with pytest.raises(SessionIntegrityError, match="source snapshot"):
|
||||
store.publish_lab_instance(**parameters)
|
||||
|
||||
|
||||
def test_lab_publication_rejects_a_source_changed_after_snapshot(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
_detail, snapshot_sha256 = store.get_session_with_catalog_snapshot(source.name)
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_session_artifacts SET byte_length = byte_length + 1 "
|
||||
"WHERE session_id = ? AND artifact_id = 'raw-transport-primary'",
|
||||
(source.name,),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="changed after admission"):
|
||||
store.publish_lab_instance(
|
||||
session_id="lab-source-snapshot-race",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB E21 · source snapshot race",
|
||||
lab_id="LAB E21",
|
||||
result_kind="source-snapshot-race",
|
||||
result_id="source-snapshot-race-result",
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
provenance={"method": lab_method()},
|
||||
expected_source_catalog_sha256=snapshot_sha256,
|
||||
)
|
||||
assert store.get_lab_instance("lab-source-snapshot-race") is None
|
||||
|
||||
|
||||
def test_delete_session_rejects_a_catalog_target_outside_its_allowed_root(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user