feat(perception): integrate calibrated operator pipeline
Add calibrated K1 projection, recorded and near-live perception qualification, unified Rerun operator layers, bounded replay admission, audited viewer controls, worker experiments, and lab evidence.
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
calibrated: true
|
||||
camera_0:
|
||||
camera_model: kb4
|
||||
camera_pose: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
|
||||
distortion: [-0.02, -0.0015, -0.002, 0.001]
|
||||
image_height: 3000
|
||||
image_width: 4000
|
||||
intrinsic: [968, 968, 2000, 1500]
|
||||
camera_1:
|
||||
camera_model: kb4
|
||||
camera_pose: [-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, -0.07, 0, 0, 0, 1]
|
||||
distortion: [-0.021, -0.0014, -0.0019, 0.0009]
|
||||
image_height: 3000
|
||||
image_width: 4000
|
||||
intrinsic: [970, 971, 1980, 1510]
|
||||
camera_2:
|
||||
camera_model: kb4
|
||||
camera_pose: [1, 0, 0, 0.036, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
|
||||
distortion: [-0.002, 0.004, 0.003, -0.001]
|
||||
image_height: 800
|
||||
image_width: 1280
|
||||
intrinsic: [386, 386, 640, 400]
|
||||
camera_3:
|
||||
camera_model: kb4
|
||||
camera_pose: [-1, 0, 0, 0.036, 0, 1, 0, 0, 0, 0, -1, -0.07, 0, 0, 0, 1]
|
||||
distortion: [-0.0021, 0.0041, 0.0031, -0.0011]
|
||||
image_height: 800
|
||||
image_width: 1280
|
||||
intrinsic: [387, 387, 641, 401]
|
||||
version: V2.2.0_alpha
|
||||
@@ -0,0 +1,3 @@
|
||||
calibrated: true
|
||||
transform: [1, 0, 0, -0.007, 0, 0, -1, -0.0948, 0, 1, 0, -0.0372, 0, 0, 0, 1]
|
||||
version: V2.2.0_alpha
|
||||
@@ -0,0 +1,199 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from PIL import Image, ImageDraw
|
||||
from test_perception_qualification import _evaluation_fixture
|
||||
|
||||
from k1link.compute import (
|
||||
AnnotationWorkspaceError,
|
||||
prepare_annotation_workspace,
|
||||
prepare_recorded_evaluation_pack,
|
||||
validate_annotation_workspace,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.analyze.valid_fov import validate_k1_valid_fov_mask
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _write_json(path: Path, payload: object) -> None:
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _artifact(path: Path, root: Path) -> dict[str, Any]:
|
||||
return {
|
||||
"path": path.relative_to(root).as_posix(),
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _prelabel_fixture(pack_root: Path, valid_fov_root: Path, output_root: Path) -> Path:
|
||||
pack = json.loads((pack_root / "manifest.json").read_text(encoding="utf-8"))
|
||||
valid_fov = validate_k1_valid_fov_mask(valid_fov_root)
|
||||
staging = output_root / "staging"
|
||||
(staging / "instance-prelabels").mkdir(parents=True)
|
||||
(staging / "semantic-prelabels").mkdir()
|
||||
with Image.open(valid_fov.mask_path) as opened:
|
||||
valid_mask = opened.copy()
|
||||
rows: list[dict[str, Any]] = []
|
||||
for order, source in enumerate(pack["identity"]["frames"], start=1):
|
||||
instance = Image.new("I;16", (valid_fov.width, valid_fov.height), 0)
|
||||
ImageDraw.Draw(instance).rectangle((390, 290, 409, 309), fill=1)
|
||||
instance.save(staging / "instance-prelabels" / f"image-{order:03d}.png")
|
||||
semantic = Image.composite(
|
||||
Image.new("L", (valid_fov.width, valid_fov.height), 7),
|
||||
Image.new("L", (valid_fov.width, valid_fov.height), 0),
|
||||
valid_mask,
|
||||
)
|
||||
ImageDraw.Draw(semantic).rectangle((390, 290, 409, 309), fill=4)
|
||||
semantic.save(staging / "semantic-prelabels" / f"image-{order:03d}.png")
|
||||
rows.append(
|
||||
{
|
||||
"schema_version": "missioncore.perception-evaluation-prelabel-frame/v1",
|
||||
"image_id": source["image_id"],
|
||||
"frame_index": source["frame_index"],
|
||||
"session_seconds": source["session_seconds"],
|
||||
"role": source["role"],
|
||||
"group_id": source["group_id"],
|
||||
"instances": [
|
||||
{
|
||||
"instance_id": 1,
|
||||
"draft_category_id": 4,
|
||||
"draft_category": "car",
|
||||
"source_model_category_id": 3,
|
||||
"source_model_category": "car",
|
||||
"score": 0.9,
|
||||
"box_xyxy": [390.0, 290.0, 410.0, 310.0],
|
||||
"mask_pixels": 400,
|
||||
"review_state": "unreviewed-model-draft",
|
||||
}
|
||||
],
|
||||
"review_state": "unreviewed-model-draft",
|
||||
}
|
||||
)
|
||||
(staging / "frames.jsonl").write_text(
|
||||
"".join(json.dumps(row, separators=(",", ":")) + "\n" for row in rows),
|
||||
encoding="utf-8",
|
||||
)
|
||||
identity = {
|
||||
"schema_version": "missioncore.perception-evaluation-prelabels-identity/v1",
|
||||
"evaluation_pack_id": pack["generation_id"],
|
||||
"evaluation_identity_sha256": pack["identity_sha256"],
|
||||
"valid_fov_generation_id": valid_fov.generation_id,
|
||||
"pipeline": "fixture/v1",
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"evaluation-prelabels-{identity_sha256}"
|
||||
artifacts = [
|
||||
_artifact(path, staging) for path in sorted(staging.rglob("*")) if path.is_file()
|
||||
]
|
||||
_write_json(
|
||||
staging / "result.json",
|
||||
{
|
||||
"schema_version": "missioncore.perception-evaluation-prelabels/v1",
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"review_state": "unreviewed-model-draft",
|
||||
"artifacts": artifacts,
|
||||
},
|
||||
)
|
||||
final = output_root / result_id
|
||||
staging.rename(final)
|
||||
return final
|
||||
|
||||
|
||||
def _workspace_fixture(tmp_path: Path) -> tuple[Path, Path, Path, Path]:
|
||||
job_root, qualification_root, valid_fov_root, frames_root, timeline_path, requests = (
|
||||
_evaluation_fixture(tmp_path)
|
||||
)
|
||||
pack = prepare_recorded_evaluation_pack(
|
||||
job_root=job_root,
|
||||
qualification_root=qualification_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
decoded_frames_root=frames_root,
|
||||
timeline_path=timeline_path,
|
||||
output_root=tmp_path / "evaluation-packs",
|
||||
selection=requests,
|
||||
decoder_version="fixture-decoder/v1",
|
||||
selection_document_sha256="1" * 64,
|
||||
producer_files=(("fixture.py", "2" * 64),),
|
||||
)
|
||||
prelabels = _prelabel_fixture(pack.root, valid_fov_root, tmp_path / "prelabels")
|
||||
return pack.root, prelabels, valid_fov_root, tmp_path / "annotation-workspaces"
|
||||
|
||||
|
||||
def test_annotation_workspace_is_reproducible_and_remains_unreviewed(tmp_path: Path) -> None:
|
||||
pack_root, prelabels_root, valid_fov_root, output_root = _workspace_fixture(tmp_path)
|
||||
first = prepare_annotation_workspace(
|
||||
evaluation_pack_root=pack_root,
|
||||
prelabels_root=prelabels_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
output_root=output_root,
|
||||
producer_files=(("fixture.py", "3" * 64),),
|
||||
)
|
||||
repeated = prepare_annotation_workspace(
|
||||
evaluation_pack_root=pack_root,
|
||||
prelabels_root=prelabels_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
output_root=output_root,
|
||||
producer_files=(("fixture.py", "3" * 64),),
|
||||
)
|
||||
|
||||
assert repeated == first
|
||||
assert first.frame_count == 22
|
||||
assert first.draft_instance_count == 22
|
||||
manifest = json.loads(first.manifest_path.read_text(encoding="utf-8"))
|
||||
assert manifest["ground_truth"] is False
|
||||
assert manifest["state"] == "prepared-unreviewed-model-draft"
|
||||
review = json.loads(first.review_template_path.read_text(encoding="utf-8"))
|
||||
assert {row["review_status"] for row in review["frames"]} == {"unreviewed"}
|
||||
|
||||
with zipfile.ZipFile(first.instance_archive_path) as archive:
|
||||
coco = json.loads(archive.read("annotations/instances_default.json"))
|
||||
assert len(coco["images"]) == 22
|
||||
assert len(coco["annotations"]) == 22
|
||||
assert all(sum(row["segmentation"]["counts"]) == 800 * 600 for row in coco["annotations"])
|
||||
assert all(row["area"] == 400 for row in coco["annotations"])
|
||||
assert (
|
||||
validate_annotation_workspace(
|
||||
first.root,
|
||||
evaluation_pack_root=pack_root,
|
||||
prelabels_root=prelabels_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
)
|
||||
== first
|
||||
)
|
||||
|
||||
|
||||
def test_annotation_workspace_rejects_changed_archive(tmp_path: Path) -> None:
|
||||
pack_root, prelabels_root, valid_fov_root, output_root = _workspace_fixture(tmp_path)
|
||||
result = prepare_annotation_workspace(
|
||||
evaluation_pack_root=pack_root,
|
||||
prelabels_root=prelabels_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
output_root=output_root,
|
||||
producer_files=(("fixture.py", "3" * 64),),
|
||||
)
|
||||
result.semantic_archive_path.write_bytes(b"changed")
|
||||
|
||||
with pytest.raises(AnnotationWorkspaceError, match="artifact changed"):
|
||||
validate_annotation_workspace(result.root)
|
||||
@@ -1,7 +1,14 @@
|
||||
import asyncio
|
||||
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.ble.scanner import advertisement_record
|
||||
from k1link.device_plugins.xgrids_k1.ble.scanner import (
|
||||
advertisement_record,
|
||||
discovered_device,
|
||||
scan,
|
||||
)
|
||||
|
||||
|
||||
def test_advertisement_record_marks_k1_candidate() -> None:
|
||||
@@ -37,3 +44,31 @@ def test_advertisement_record_marks_synthetic_xgr_name_as_k1_candidate() -> None
|
||||
|
||||
record = advertisement_record(device, advertisement)
|
||||
assert record["k1_name_candidate"] is True
|
||||
|
||||
|
||||
def test_scan_retains_the_live_corebluetooth_handle(
|
||||
monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
device = BLEDevice("LIVE-UUID", "XGR-LIVE", details=object())
|
||||
advertisement = AdvertisementData(
|
||||
local_name="XGR-LIVE",
|
||||
manufacturer_data={},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
tx_power=0,
|
||||
rssi=-41,
|
||||
platform_data=(),
|
||||
)
|
||||
|
||||
async def fake_discover(**_kwargs: object) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
|
||||
return {"LIVE-UUID": (device, advertisement)}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"k1link.device_plugins.xgrids_k1.ble.scanner.BleakScanner.discover",
|
||||
fake_discover,
|
||||
)
|
||||
|
||||
result = asyncio.run(scan(1.0))
|
||||
|
||||
assert result["devices"][0]["macos_uuid"] == "LIVE-UUID"
|
||||
assert discovered_device("LIVE-UUID") is device
|
||||
|
||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import k1link.web.camera_archive as camera_archive_module
|
||||
from k1link.device_plugins.xgrids_k1.archive import discover_legacy_viewer_sessions
|
||||
from k1link.web.camera_archive import (
|
||||
CAMERA_ARCHIVE_SCHEMA,
|
||||
@@ -81,6 +82,84 @@ def test_camera_archive_writes_canonical_segments_and_seals_summary(tmp_path: Pa
|
||||
assert writer.close() == summary
|
||||
|
||||
|
||||
def test_recovery_accepts_sealed_index_larger_than_old_duration_cap_without_loading_video(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
sessions_root = tmp_path / "sessions"
|
||||
epoch = (
|
||||
sessions_root
|
||||
/ "20260720T065719Z_viewer_live"
|
||||
/ "media"
|
||||
/ "sensor.camera.right"
|
||||
/ "epoch-1"
|
||||
)
|
||||
segments = epoch / "segments"
|
||||
segments.mkdir(parents=True)
|
||||
init = b"sealed-init"
|
||||
(epoch / "init.mp4").write_bytes(init)
|
||||
segment_count = 560
|
||||
segment = b"x"
|
||||
segment_sha256 = hashlib.sha256(segment).hexdigest()
|
||||
index_sha256 = hashlib.sha256()
|
||||
with (epoch / "index.jsonl").open("wb") as stream:
|
||||
for sequence in range(1, segment_count + 1):
|
||||
(segments / f"{sequence}.m4s").write_bytes(segment)
|
||||
line = (
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording-index/v1",
|
||||
"sequence": sequence,
|
||||
"kind": "media",
|
||||
"path": f"segments/{sequence}.m4s",
|
||||
"length": 1,
|
||||
"sha256": segment_sha256,
|
||||
"padding": "p" * 60_000,
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
+ "\n"
|
||||
).encode()
|
||||
index_sha256.update(line)
|
||||
stream.write(line)
|
||||
assert (epoch / "index.jsonl").stat().st_size > 32 * 1024 * 1024
|
||||
(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,
|
||||
"valid_bytes": len(init) + segment_count,
|
||||
"init_sha256": hashlib.sha256(init).hexdigest(),
|
||||
"stream_sha256": hashlib.sha256(init + segment * segment_count).hexdigest(),
|
||||
"index_sha256": index_sha256.hexdigest(),
|
||||
"synchronization": "host-arrival-best-effort",
|
||||
"commit_policy": CAMERA_COMMIT_POLICY,
|
||||
"artifacts": {
|
||||
"init": "init.mp4",
|
||||
"segments": "segments",
|
||||
"index": "index.jsonl",
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def fail_if_recovery_reads_video(_segments_fd: int) -> object:
|
||||
raise AssertionError("a clean sealed archive must not enter payload recovery")
|
||||
|
||||
monkeypatch.setattr(
|
||||
camera_archive_module,
|
||||
"_read_recovery_segment_catalog",
|
||||
fail_if_recovery_reads_video,
|
||||
)
|
||||
assert recover_incomplete_camera_archives(sessions_root) == ()
|
||||
|
||||
|
||||
def test_camera_archive_rejects_media_without_init_and_existing_epoch(tmp_path: Path) -> None:
|
||||
session = tmp_path / "session"
|
||||
session.mkdir()
|
||||
|
||||
@@ -5,12 +5,19 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import k1link.compute.fusion_epoch as fusion_module
|
||||
import k1link.compute.perception_epoch as epoch_module
|
||||
import k1link.compute.results as result_module
|
||||
from k1link.compute import (
|
||||
RecordedCalibratedFusionStore,
|
||||
RecordedPerceptionEpochStore,
|
||||
RecordedPerceptionOverlayStore,
|
||||
prepare_camera_compute_job,
|
||||
validate_recorded_calibrated_fusion,
|
||||
validate_recorded_perception_epoch_result,
|
||||
validate_recorded_perception_result,
|
||||
)
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
@@ -164,6 +171,166 @@ def _result(tmp_path: Path, job: Any) -> Path:
|
||||
return root
|
||||
|
||||
|
||||
def _epoch_result(tmp_path: Path, job: Any) -> Path:
|
||||
identity = {
|
||||
"schema_version": "missioncore.recorded-perception-identity/v2",
|
||||
"job_id": job.job_id,
|
||||
"input_sha256": job.input_sha256,
|
||||
"calibration": {
|
||||
"content_identity_sha256": "b" * 64,
|
||||
"camera_slot": "camera_1",
|
||||
},
|
||||
"configuration": {"pipeline": "synthetic-panoptic/v1"},
|
||||
"models": {"instance": {"id": "synthetic"}},
|
||||
"publication": {
|
||||
"video_encoder": "synthetic",
|
||||
"video_media_type": "video/mp4",
|
||||
"mask_archive_media_type": "application/gzip",
|
||||
},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"result-{identity_sha256}"
|
||||
root = tmp_path / "results" / job.job_id / result_id
|
||||
root.mkdir(parents=True)
|
||||
frame = {
|
||||
"schema_version": "missioncore.panoptic-frame/v1",
|
||||
"frame_index": 0,
|
||||
"sequence": 1,
|
||||
"session_seconds": job.timeline_start_seconds,
|
||||
"instances": [],
|
||||
"semantic_classes": [],
|
||||
}
|
||||
(root / "frames.jsonl").write_text(
|
||||
json.dumps(frame, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "perception.mp4").write_bytes(b"synthetic-mp4")
|
||||
(root / "masks.tar.gz").write_bytes(b"synthetic-masks")
|
||||
(root / "gpu-telemetry.jsonl").write_text("{}\n", encoding="utf-8")
|
||||
report = {
|
||||
"schema_version": "missioncore.perception-run-report/v1",
|
||||
"state": "published",
|
||||
"result_id": result_id,
|
||||
"input": {"job_id": job.job_id, "input_sha256": job.input_sha256},
|
||||
"metrics": {
|
||||
"frames_expected": 1,
|
||||
"frames_processed": 1,
|
||||
"frames_failed": 0,
|
||||
"frames_skipped": 0,
|
||||
},
|
||||
}
|
||||
(root / "run-report.json").write_text(json.dumps(report) + "\n", encoding="utf-8")
|
||||
artifact_contracts = (
|
||||
("panoptic-overlay-video", "perception.mp4", "video/mp4", None),
|
||||
("panoptic-mask-archive", "masks.tar.gz", "application/gzip", None),
|
||||
(
|
||||
"panoptic-frame-metadata",
|
||||
"frames.jsonl",
|
||||
"application/x-ndjson",
|
||||
"missioncore.panoptic-frame/v1",
|
||||
),
|
||||
("worker-gpu-telemetry", "gpu-telemetry.jsonl", "application/x-ndjson", None),
|
||||
(
|
||||
"perception-run-report",
|
||||
"run-report.json",
|
||||
"application/json",
|
||||
"missioncore.perception-run-report/v1",
|
||||
),
|
||||
)
|
||||
artifacts = []
|
||||
for kind, name, media_type, schema_version in artifact_contracts:
|
||||
path = root / name
|
||||
descriptor = {
|
||||
"kind": kind,
|
||||
"path": name,
|
||||
"media_type": media_type,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
}
|
||||
if schema_version is not None:
|
||||
descriptor["schema_version"] = schema_version
|
||||
artifacts.append(descriptor)
|
||||
result = {
|
||||
"schema_version": "missioncore.recorded-perception-result/v2",
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": "2026-07-20T00:00:00.000Z",
|
||||
"job_id": job.job_id,
|
||||
"input_sha256": job.input_sha256,
|
||||
"session_id": job.session_id,
|
||||
"source_id": job.source_id,
|
||||
"codec_epoch": job.codec_epoch,
|
||||
"timestamp_basis": "session-time-seconds",
|
||||
"timeline_start_seconds": job.timeline_start_seconds,
|
||||
"timeline_end_seconds": job.timeline_end_seconds,
|
||||
"frames_processed": job.segment_count,
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
(root / "result.json").write_text(json.dumps(result) + "\n", encoding="utf-8")
|
||||
return root
|
||||
|
||||
|
||||
def _fusion_result(tmp_path: Path, job: Any, perception_root: Path) -> Path:
|
||||
perception_result_id = perception_root.name
|
||||
identity = {
|
||||
"schema_version": "missioncore.recorded-calibrated-fusion-identity/v1",
|
||||
"job_id": job.job_id,
|
||||
"input_sha256": job.input_sha256,
|
||||
"perception_result_id": perception_result_id,
|
||||
"calibration_sha256": "b" * 64,
|
||||
"camera_slot": "camera_1",
|
||||
"configuration": {"pipeline": "synthetic-fusion/v1"},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
fusion_id = f"fusion-{identity_sha256}"
|
||||
root = tmp_path / "fusions" / job.job_id / fusion_id
|
||||
root.mkdir(parents=True)
|
||||
np.savez_compressed(
|
||||
root / "fusion.npz",
|
||||
frame_times_ns=np.asarray([0], dtype=np.int64),
|
||||
point_offsets=np.asarray([0, 2], dtype=np.int64),
|
||||
points=np.asarray([[1, 2, 3], [4, 5, 6]], dtype=np.float32),
|
||||
point_colors=np.asarray([[1, 2, 3], [4, 5, 6]], dtype=np.uint8),
|
||||
box_offsets=np.asarray([0, 1], dtype=np.int64),
|
||||
box_centers=np.asarray([[1, 2, 3]], dtype=np.float32),
|
||||
box_half_sizes=np.asarray([[0.5, 0.5, 0.5]], dtype=np.float32),
|
||||
box_colors=np.asarray([[1, 2, 3, 96]], dtype=np.uint8),
|
||||
)
|
||||
(root / "box-labels.json").write_text('["person · 2.0 m · 5 pts"]', encoding="utf-8")
|
||||
(root / "fusion-frames.jsonl").write_text(
|
||||
'{"frame_index":0,"state":"fused"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
artifacts = [
|
||||
{
|
||||
"name": path.name,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
}
|
||||
for path in (
|
||||
root / "fusion.npz",
|
||||
root / "box-labels.json",
|
||||
root / "fusion-frames.jsonl",
|
||||
)
|
||||
]
|
||||
manifest = {
|
||||
"schema_version": "missioncore.recorded-calibrated-fusion/v1",
|
||||
"fusion_id": fusion_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": "2026-07-20T01:00:00.000Z",
|
||||
"session_id": job.session_id,
|
||||
"source_id": job.source_id,
|
||||
"frame_count": job.segment_count,
|
||||
"timeline_start_seconds": job.timeline_start_seconds,
|
||||
"timeline_end_seconds": job.timeline_end_seconds,
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
(root / "manifest.json").write_text(json.dumps(manifest) + "\n", encoding="utf-8")
|
||||
return root
|
||||
|
||||
|
||||
def test_result_is_bound_to_job_and_cache_reuses_complete_overlay(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -210,3 +377,138 @@ def test_result_rejects_changed_detection_payload(tmp_path: Path) -> None:
|
||||
detection_path.write_bytes(detection_path.read_bytes().replace(b"0.75", b"0.76"))
|
||||
with pytest.raises(SessionIntegrityError, match="artifact identity changed"):
|
||||
validate_recorded_perception_result(job.job_root, result_root)
|
||||
|
||||
|
||||
def test_full_epoch_result_publishes_native_panoptic_video(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
job = _job(tmp_path)
|
||||
result_root = _epoch_result(tmp_path, job)
|
||||
result = validate_recorded_perception_epoch_result(job.job_root, result_root)
|
||||
assert result.calibration_slot == "camera_1"
|
||||
assert result.artifact("panoptic-overlay-video").byte_length == len(b"synthetic-mp4")
|
||||
|
||||
monkeypatch.setattr(
|
||||
epoch_module.RecordedPerceptionEpochStore,
|
||||
"_probe_video",
|
||||
lambda *_: (0.5, "h264", 800, 600),
|
||||
)
|
||||
store = RecordedPerceptionEpochStore(
|
||||
jobs_root=tmp_path / "jobs",
|
||||
results_root=tmp_path / "results",
|
||||
ffprobe_path=Path(__file__),
|
||||
)
|
||||
video = store.video("session-1")
|
||||
assert video is not None
|
||||
assert video.result_id == result.result_id
|
||||
assert video.public_source_id == "recorded.perception.left"
|
||||
assert video.timeline_start_seconds == 0
|
||||
assert video.timeline_end_seconds == 0.5
|
||||
|
||||
|
||||
def test_full_epoch_store_reuses_unchanged_validated_result(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
job = _job(tmp_path)
|
||||
_epoch_result(tmp_path, job)
|
||||
validation_calls = 0
|
||||
validate = epoch_module.validate_recorded_perception_epoch_result
|
||||
|
||||
def count_validation(job_root: Path, result_root: Path) -> Any:
|
||||
nonlocal validation_calls
|
||||
validation_calls += 1
|
||||
return validate(job_root, result_root)
|
||||
|
||||
monkeypatch.setattr(
|
||||
epoch_module,
|
||||
"validate_recorded_perception_epoch_result",
|
||||
count_validation,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
epoch_module.RecordedPerceptionEpochStore,
|
||||
"_probe_video",
|
||||
lambda *_: (0.5, "h264", 800, 600),
|
||||
)
|
||||
store = RecordedPerceptionEpochStore(
|
||||
jobs_root=tmp_path / "jobs",
|
||||
results_root=tmp_path / "results",
|
||||
ffprobe_path=Path(__file__),
|
||||
)
|
||||
|
||||
assert store.video("session-1") is not None
|
||||
assert store.video("session-1") is not None
|
||||
assert validation_calls == 1
|
||||
|
||||
|
||||
def test_full_epoch_result_rejects_changed_frame_metadata(tmp_path: Path) -> None:
|
||||
job = _job(tmp_path)
|
||||
result_root = _epoch_result(tmp_path, job)
|
||||
frame_path = result_root / "frames.jsonl"
|
||||
frame_path.write_text("{}\n", encoding="utf-8")
|
||||
with pytest.raises(SessionIntegrityError, match="artifact identity changed"):
|
||||
validate_recorded_perception_epoch_result(job.job_root, result_root)
|
||||
|
||||
|
||||
def test_calibrated_fusion_is_bound_to_full_epoch_and_reuses_render_cache(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
job = _job(tmp_path)
|
||||
perception_root = _epoch_result(tmp_path, job)
|
||||
fusion_root = _fusion_result(tmp_path, job, perception_root)
|
||||
fusion = validate_recorded_calibrated_fusion(
|
||||
job.job_root,
|
||||
tmp_path / "results",
|
||||
fusion_root,
|
||||
)
|
||||
assert fusion.perception_result_id == perception_root.name
|
||||
assert fusion.frame_count == 1
|
||||
rendered = fusion_module._render_fusion(
|
||||
fusion,
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id="recording-render-proof",
|
||||
)
|
||||
assert rendered.startswith(b"RRF2")
|
||||
|
||||
calls = 0
|
||||
|
||||
def render(*_: object, **__: object) -> bytes:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return b"RRF2synthetic-fusion"
|
||||
|
||||
monkeypatch.setattr(fusion_module, "_render_fusion", render)
|
||||
store = RecordedCalibratedFusionStore(
|
||||
jobs_root=tmp_path / "jobs",
|
||||
perception_results_root=tmp_path / "results",
|
||||
fusion_results_root=tmp_path / "fusions",
|
||||
cache_root=tmp_path / "fusion-cache",
|
||||
)
|
||||
first = store.render(
|
||||
"session-1",
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id="recording-1",
|
||||
)
|
||||
second = store.render(
|
||||
"session-1",
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id="recording-1",
|
||||
)
|
||||
assert first == second == b"RRF2synthetic-fusion"
|
||||
assert calls == 1
|
||||
|
||||
|
||||
def test_calibrated_fusion_rejects_changed_array_artifact(tmp_path: Path) -> None:
|
||||
job = _job(tmp_path)
|
||||
perception_root = _epoch_result(tmp_path, job)
|
||||
fusion_root = _fusion_result(tmp_path, job, perception_root)
|
||||
with (fusion_root / "fusion.npz").open("ab") as stream:
|
||||
stream.write(b"changed")
|
||||
with pytest.raises(SessionIntegrityError, match="artifact identity changed"):
|
||||
validate_recorded_calibrated_fusion(
|
||||
job.job_root,
|
||||
tmp_path / "results",
|
||||
fusion_root,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.compute.integrated_perception import _CuboidPresentationState
|
||||
|
||||
|
||||
def _worker_modules() -> tuple[object, object]:
|
||||
root = Path(__file__).resolve().parents[1] / "experiments" / "perception"
|
||||
worker = root / "worker"
|
||||
sys.path.insert(0, str(root))
|
||||
sys.path.insert(0, str(worker))
|
||||
try:
|
||||
fusion_spec = importlib.util.spec_from_file_location(
|
||||
"e10_test_fusion", root / "e10_fusion_runtime.py"
|
||||
)
|
||||
assert fusion_spec is not None and fusion_spec.loader is not None
|
||||
fusion = importlib.util.module_from_spec(fusion_spec)
|
||||
sys.modules[fusion_spec.name] = fusion
|
||||
fusion_spec.loader.exec_module(fusion)
|
||||
runner_spec = importlib.util.spec_from_file_location(
|
||||
"e10_test_runner", worker / "run_e10_integrated_perception.py"
|
||||
)
|
||||
assert runner_spec is not None and runner_spec.loader is not None
|
||||
runner = importlib.util.module_from_spec(runner_spec)
|
||||
sys.modules[runner_spec.name] = runner
|
||||
runner_spec.loader.exec_module(runner)
|
||||
return fusion, runner
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def test_e10_profile_pins_integrated_realtime_budget() -> None:
|
||||
_fusion, runner = _worker_modules()
|
||||
profile_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e10_integrated_perception_profile.json"
|
||||
)
|
||||
profile, digest = runner.read_profile(profile_path)
|
||||
assert len(digest) == 64
|
||||
assert profile["replay"] == {
|
||||
"speed": 1.0,
|
||||
"detector_queue_capacity": 2,
|
||||
"semantic_queue_capacity": 1,
|
||||
"semantic_sample_every_frames": 5,
|
||||
"semantic_ttl_ms": 750.0,
|
||||
}
|
||||
assert profile["acceptance"]["detector_minimum_effective_fps"] == 9.5
|
||||
assert profile["acceptance"]["maximum_p95_world_state_age_ms"] == 175.0
|
||||
assert profile["acceptance"]["minimum_lidar_fused_frames"] == 450
|
||||
|
||||
|
||||
def test_e10_semantic_loss_profile_is_explicit_and_fail_closed() -> None:
|
||||
_fusion, runner = _worker_modules()
|
||||
profile_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e10_semantic_loss_profile.json"
|
||||
)
|
||||
profile, digest = runner.read_profile(profile_path)
|
||||
assert len(digest) == 64
|
||||
assert profile["mode"] == "semantic-loss-negative-control"
|
||||
assert profile["semantic_loss"]["stop_after_completed_results"] == 20
|
||||
assert profile["acceptance"]["minimum_stale_detector_frames"] == 450
|
||||
assert profile["replay"]["semantic_ttl_ms"] == 750.0
|
||||
|
||||
|
||||
def test_e10_full_session_profile_pins_complete_camera_epoch() -> None:
|
||||
_fusion, runner = _worker_modules()
|
||||
profile_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e10_full_session_profile.json"
|
||||
)
|
||||
profile, digest = runner.read_profile(profile_path)
|
||||
assert len(digest) == 64
|
||||
assert profile["mode"] == "full-session-qualification"
|
||||
assert profile["selection"] == {
|
||||
"required_frame_count": 4489,
|
||||
"required_source_start_frame_index": 0,
|
||||
"required_source_end_frame_index": 4488,
|
||||
"minimum_source_span_seconds": 448.0,
|
||||
}
|
||||
assert profile["acceptance"]["detector_maximum_drop_fraction"] == 0.0
|
||||
assert profile["acceptance"]["minimum_lidar_fused_frames"] == 3500
|
||||
|
||||
|
||||
def test_e13_profile_pins_provenance_marked_amodal_completion() -> None:
|
||||
_fusion, runner = _worker_modules()
|
||||
profile_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e13_amodal_cuboid_profile.json"
|
||||
)
|
||||
|
||||
profile, digest = runner.read_profile(profile_path)
|
||||
|
||||
assert len(digest) == 64
|
||||
assert profile["mode"] == "pilot"
|
||||
assert profile["cuboid_completion"]["mode"] == "class-prior-amodal-v1"
|
||||
assert profile["cuboid_completion"]["classes"]["car"]["nominal_size_m"] == [
|
||||
4.5,
|
||||
1.85,
|
||||
1.55,
|
||||
]
|
||||
assert profile["cuboid_completion"]["temporal"]["confirmation_hits"] == 3
|
||||
|
||||
|
||||
def test_e14_profile_combines_full_session_and_amodal_gates() -> None:
|
||||
_fusion, runner = _worker_modules()
|
||||
profile_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e14_full_session_amodal_profile.json"
|
||||
)
|
||||
|
||||
profile, digest = runner.read_profile(profile_path)
|
||||
|
||||
assert len(digest) == 64
|
||||
assert profile["mode"] == "full-session-qualification"
|
||||
assert profile["selection"] == {
|
||||
"required_frame_count": 4489,
|
||||
"required_source_start_frame_index": 0,
|
||||
"required_source_end_frame_index": 4488,
|
||||
"minimum_source_span_seconds": 448.0,
|
||||
}
|
||||
assert profile["cuboid_completion"]["mode"] == "class-prior-amodal-v1"
|
||||
assert profile["cuboid_completion"]["failure_policy"] == "reject"
|
||||
assert profile["acceptance"]["minimum_lidar_fused_frames"] == 3500
|
||||
assert profile["acceptance"]["minimum_accepted_cuboids"] == 1500
|
||||
|
||||
|
||||
def test_recorded_cuboid_presentation_holds_one_failed_association_then_expires() -> None:
|
||||
state = _CuboidPresentationState(hold_ns=500_000_000)
|
||||
accepted = {
|
||||
"association_group": "vehicle",
|
||||
"clustered_points": 14,
|
||||
"cuboid_status": "accepted-class-prior-amodal-v1",
|
||||
"distance_smoothed_m": 8.25,
|
||||
"label": "car",
|
||||
"track_id": 7,
|
||||
}
|
||||
initial = state.update(
|
||||
1_000_000_000,
|
||||
[accepted],
|
||||
np.asarray([[1.0, 2.0, 3.0]], dtype=np.float32),
|
||||
np.asarray([[2.25, 0.925, 0.775]], dtype=np.float32),
|
||||
np.asarray([[0.0, 0.0, 0.0, 1.0]], dtype=np.float32),
|
||||
np.asarray([[118, 204, 132, 88]], dtype=np.uint8),
|
||||
)
|
||||
assert initial is not None
|
||||
assert initial[4] == ["vehicle #7 car · 8.2 m · 14 pts"]
|
||||
|
||||
rejected = {"cuboid_status": "rejected-no-semantic-lidar-support", "track_id": 7}
|
||||
held = state.update(
|
||||
1_200_000_000,
|
||||
[rejected],
|
||||
np.empty((0, 3), dtype=np.float32),
|
||||
np.empty((0, 3), dtype=np.float32),
|
||||
np.empty((0, 4), dtype=np.float32),
|
||||
np.empty((0, 4), dtype=np.uint8),
|
||||
)
|
||||
assert held is not None
|
||||
assert held[4] == ["vehicle #7 car · 8.2 m · 14 pts · hold 200 ms"]
|
||||
assert int(held[3][0, 3]) < 88
|
||||
|
||||
expired = state.update(
|
||||
1_600_000_001,
|
||||
[],
|
||||
np.empty((0, 3), dtype=np.float32),
|
||||
np.empty((0, 3), dtype=np.float32),
|
||||
np.empty((0, 4), dtype=np.float32),
|
||||
np.empty((0, 4), dtype=np.uint8),
|
||||
)
|
||||
assert expired is None
|
||||
|
||||
|
||||
def test_e13_completes_a_visible_car_face_away_from_the_sensor() -> None:
|
||||
fusion, runner = _worker_modules()
|
||||
profile, _digest = runner.read_profile(
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e13_amodal_cuboid_profile.json"
|
||||
)
|
||||
y = np.linspace(-0.8, 0.8, 12)
|
||||
z = np.linspace(0.35, 1.3, 5)
|
||||
support = np.asarray(
|
||||
[[10.0 + 0.01 * (index % 2), side, height] for index, side in enumerate(y) for height in z],
|
||||
dtype=np.float64,
|
||||
)
|
||||
ground = np.asarray(
|
||||
[[x, side, 0.0] for x in np.linspace(8.0, 12.0, 8) for side in (-1.5, 0.0, 1.5)],
|
||||
dtype=np.float64,
|
||||
)
|
||||
observed = fusion._cuboid(support, profile["association"], "vehicle")
|
||||
assert observed is not None
|
||||
tracker = fusion.CuboidCompletionTracker(profile["cuboid_completion"])
|
||||
|
||||
completed = tracker.complete(
|
||||
track_id=4,
|
||||
label="car",
|
||||
support_points_map=support,
|
||||
all_points_map=np.concatenate((support, ground)),
|
||||
sensor_position_map=(0.0, 0.0, 0.0),
|
||||
session_seconds=1.0,
|
||||
observed_cuboid=observed,
|
||||
)
|
||||
|
||||
assert completed is not None
|
||||
assert completed.orientation_source == "support-face-normal"
|
||||
completed_size = np.asarray(completed.cuboid.half_size) * 2.0
|
||||
assert completed_size[0] == 4.5
|
||||
assert 1.85 <= completed_size[1] <= 2.0
|
||||
assert completed_size[2] == 1.55
|
||||
assert completed.cuboid.center_map[0] > 12.0
|
||||
assert completed.ground_z_map == 0.0
|
||||
assert completed.completion_fraction > 0.8
|
||||
assert completed.support_coverage_fraction >= 0.75
|
||||
|
||||
|
||||
def test_e13_temporal_filter_reduces_cuboid_center_jitter() -> None:
|
||||
fusion, runner = _worker_modules()
|
||||
profile, _digest = runner.read_profile(
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e13_amodal_cuboid_profile.json"
|
||||
)
|
||||
tracker = fusion.CuboidCompletionTracker(profile["cuboid_completion"])
|
||||
base = np.asarray(
|
||||
[
|
||||
[x, 2.0 + 0.02 * (index % 2), z]
|
||||
for index, x in enumerate(np.linspace(8.0, 11.5, 20))
|
||||
for z in (0.4, 0.9, 1.3)
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
ground = np.asarray([[x, y, 0.0] for x in np.linspace(7.0, 13.0, 10) for y in (0.5, 2.0, 3.5)])
|
||||
|
||||
outputs = []
|
||||
for frame, lateral_jitter in enumerate((0.0, 0.4, 0.2), start=1):
|
||||
support = base + np.asarray([0.0, lateral_jitter, 0.0])
|
||||
observed = fusion._cuboid(support, profile["association"], "vehicle")
|
||||
assert observed is not None
|
||||
value = tracker.complete(
|
||||
track_id=9,
|
||||
label="car",
|
||||
support_points_map=support,
|
||||
all_points_map=np.concatenate((support, ground)),
|
||||
sensor_position_map=(0.0, 0.0, 0.0),
|
||||
session_seconds=frame * 0.1,
|
||||
observed_cuboid=observed,
|
||||
)
|
||||
assert value is not None
|
||||
outputs.append(value)
|
||||
|
||||
lateral_shift = abs(outputs[1].cuboid.center_map[1] - outputs[0].cuboid.center_map[1])
|
||||
assert lateral_shift < 0.4
|
||||
assert outputs[-1].temporal_status == "confirmed"
|
||||
|
||||
|
||||
def test_e10_semantic_binding_is_explicit_about_freshness() -> None:
|
||||
_fusion, runner = _worker_modules()
|
||||
assert runner.semantic_binding(None, 2.0, 750.0) == ("unavailable", None)
|
||||
value = runner.SemanticResult(
|
||||
frame_index=1,
|
||||
source_frame_index=1001,
|
||||
session_seconds=1.0,
|
||||
completion_age_ms=200.0,
|
||||
completed_monotonic=3.0,
|
||||
mask=np.zeros((600, 800), dtype=np.uint8),
|
||||
mask_sha256="0" * 64,
|
||||
class_pixels={},
|
||||
)
|
||||
assert runner.semantic_binding(value, 1.7, 750.0) == ("fresh", 700.0)
|
||||
assert runner.semantic_binding(value, 1.8, 750.0) == (
|
||||
"stale",
|
||||
800.0,
|
||||
)
|
||||
|
||||
|
||||
def test_e10_projection_keeps_only_nearest_point_per_pixel() -> None:
|
||||
fusion, _runner = _worker_modules()
|
||||
profile = fusion.ProjectionProfile(
|
||||
width=800,
|
||||
height=600,
|
||||
intrinsic_fx_fy_cx_cy=(100.0, 100.0, 400.0, 300.0),
|
||||
distortion_kb4=(0.0, 0.0, 0.0, 0.0),
|
||||
t_camera_from_lidar=np.eye(4, dtype=np.float64),
|
||||
)
|
||||
points = np.asarray([[0.0, 0.0, 2.0], [0.0, 0.0, 3.0]], dtype=np.float64)
|
||||
pixels, depths, source, _lidar = fusion.project_points(
|
||||
points,
|
||||
(0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
profile,
|
||||
)
|
||||
assert pixels.shape == (1, 2)
|
||||
assert depths.tolist() == [2.0]
|
||||
assert source.tolist() == [0]
|
||||
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def _module() -> object:
|
||||
path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "prepare_e15_projection_pack.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("e15_projection_pack_test", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_projection_pack_contains_calibration_only(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
calibration_sha256 = "a" * 64
|
||||
source = tmp_path / "source-pack"
|
||||
source.mkdir()
|
||||
arrays = source / "lidar-pack.npz"
|
||||
np.savez(
|
||||
arrays,
|
||||
frame_indices=np.asarray([0]),
|
||||
cloud_points_map=np.asarray([[99.0, 99.0, 99.0]]),
|
||||
intrinsic_fx_fy_cx_cy=np.asarray([1.0, 2.0, 3.0, 4.0]),
|
||||
distortion_kb4=np.asarray([0.1, 0.2, 0.3, 0.4]),
|
||||
t_camera_from_lidar=np.eye(4),
|
||||
)
|
||||
manifest = {
|
||||
"pack_id": "source-pack",
|
||||
"identity_sha256": "b" * 64,
|
||||
"identity": {
|
||||
"calibration_sha256": calibration_sha256,
|
||||
"source_id": "sensor.camera.right",
|
||||
"camera_slot": "camera_1",
|
||||
},
|
||||
"artifact": {"sha256": module._sha256(arrays)},
|
||||
}
|
||||
(source / "manifest.json").write_text(json.dumps(manifest))
|
||||
|
||||
output = module.build_projection_pack(
|
||||
source,
|
||||
tmp_path / "output",
|
||||
expected_calibration_sha256=calibration_sha256,
|
||||
)
|
||||
document = module.validate_projection_pack(output, calibration_sha256)
|
||||
|
||||
assert document["classification"] == "calibration-only-no-recorded-sensor-frames"
|
||||
with np.load(output / "projection.npz", allow_pickle=False) as packed:
|
||||
assert set(packed.files) == module.PACK_ARRAYS
|
||||
assert "cloud_points_map" not in packed.files
|
||||
assert "frame_indices" not in packed.files
|
||||
|
||||
|
||||
def test_projection_pack_rejects_changed_calibration(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
source = tmp_path / "source-pack"
|
||||
source.mkdir()
|
||||
arrays = source / "lidar-pack.npz"
|
||||
np.savez(
|
||||
arrays,
|
||||
intrinsic_fx_fy_cx_cy=np.ones(4),
|
||||
distortion_kb4=np.ones(4),
|
||||
t_camera_from_lidar=np.eye(4),
|
||||
)
|
||||
(source / "manifest.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"identity": {
|
||||
"calibration_sha256": "a" * 64,
|
||||
"source_id": "sensor.camera.right",
|
||||
"camera_slot": "camera_1",
|
||||
},
|
||||
"artifact": {"sha256": module._sha256(arrays)},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="calibration binding changed"):
|
||||
module.build_projection_pack(
|
||||
source,
|
||||
tmp_path / "output",
|
||||
expected_calibration_sha256="b" * 64,
|
||||
)
|
||||
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _module() -> object:
|
||||
perception = Path(__file__).resolve().parents[1] / "experiments" / "perception"
|
||||
worker = perception / "worker"
|
||||
sys.path.insert(0, str(perception))
|
||||
sys.path.insert(0, str(worker))
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"e15_shadow_inference_test",
|
||||
worker / "run_e15_shadow_inference.py",
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def test_e15_profile_pins_replay_shadow_authority_and_bounded_runtime() -> None:
|
||||
module = _module()
|
||||
profile_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e15_shadow_inference_profile.json"
|
||||
)
|
||||
|
||||
profile, digest = module.read_live_profile(profile_path)
|
||||
|
||||
assert len(digest) == 64
|
||||
assert profile["mode"] == "replay-shadow-gate"
|
||||
assert profile["authority"] == {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
assert profile["transport"] == {
|
||||
"wire_schema": "missioncore.live-perception-wire/v1",
|
||||
"camera_media": "persistent-fmp4-pyav",
|
||||
"pyav_version": "18.0.0",
|
||||
"maximum_media_buffer_bytes": 8 * 1024 * 1024,
|
||||
"camera_metadata_capacity": 16,
|
||||
}
|
||||
assert profile["scheduling"] == {
|
||||
"detector_queue_capacity": 2,
|
||||
"semantic_queue_capacity": 1,
|
||||
"semantic_sample_every_frames": 5,
|
||||
"semantic_ttl_ms": 750.0,
|
||||
"sensor_wait_ms": 90.0,
|
||||
}
|
||||
assert profile["acceptance"]["minimum_fused_fraction"] == 0.85
|
||||
assert profile["acceptance"]["maximum_p95_world_state_age_ms"] == 200.0
|
||||
|
||||
|
||||
def test_e15_profile_rejects_command_authority(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
source = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e15_shadow_inference_profile.json"
|
||||
)
|
||||
value = json.loads(source.read_text())
|
||||
value["authority"]["commands_enabled"] = True
|
||||
changed = tmp_path / "unsafe-profile.json"
|
||||
changed.write_text(json.dumps(value))
|
||||
|
||||
with pytest.raises(RuntimeError, match="profile contract"):
|
||||
module.read_live_profile(changed)
|
||||
|
||||
|
||||
def test_persistent_worker_request_is_single_run_d_backed_and_token_bounded(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
module = _module()
|
||||
service = Namespace(
|
||||
output_root=tmp_path,
|
||||
listen_host="127.0.0.1",
|
||||
listen_port=18020,
|
||||
command="serve",
|
||||
marker="preserved",
|
||||
)
|
||||
|
||||
run = module._persistent_run_arguments(
|
||||
service,
|
||||
{
|
||||
"request_id": "physical-k1-shadow-001",
|
||||
"output_name": "physical-k1-shadow-001",
|
||||
"token": "a" * 64,
|
||||
},
|
||||
)
|
||||
|
||||
assert run.command == "run"
|
||||
assert run.output == tmp_path / "physical-k1-shadow-001"
|
||||
assert run.token == "a" * 64
|
||||
assert run.token_stdin is False
|
||||
assert run.marker == "preserved"
|
||||
assert not hasattr(run, "listen_host")
|
||||
assert not hasattr(run, "output_root")
|
||||
|
||||
with pytest.raises(RuntimeError, match="request contract"):
|
||||
module._persistent_run_arguments(
|
||||
service,
|
||||
{
|
||||
"request_id": "physical-k1-shadow-002",
|
||||
"output_name": "physical-k1-shadow-002",
|
||||
"token": "short",
|
||||
},
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="request contract"):
|
||||
module._persistent_run_arguments(
|
||||
service,
|
||||
{
|
||||
"request_id": "physical-k1-shadow-003",
|
||||
"output_name": "../escape",
|
||||
"token": "b" * 64,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_RUNTIME_PATH = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e15_shadow_runtime.py"
|
||||
)
|
||||
_SPEC = importlib.util.spec_from_file_location("e15_shadow_runtime_test", _RUNTIME_PATH)
|
||||
assert _SPEC is not None and _SPEC.loader is not None
|
||||
_RUNTIME = importlib.util.module_from_spec(_SPEC)
|
||||
sys.modules[_SPEC.name] = _RUNTIME
|
||||
_SPEC.loader.exec_module(_RUNTIME)
|
||||
CameraFragmentMetadata = _RUNTIME.CameraFragmentMetadata
|
||||
IncrementalMediaBuffer = _RUNTIME.IncrementalMediaBuffer
|
||||
PersistentFmp4Decoder = _RUNTIME.PersistentFmp4Decoder
|
||||
ShadowRuntimeError = _RUNTIME.ShadowRuntimeError
|
||||
|
||||
|
||||
def _metadata(sequence: int) -> CameraFragmentMetadata:
|
||||
return CameraFragmentMetadata(
|
||||
ingress_sequence=sequence,
|
||||
source_sequence=sequence,
|
||||
captured_at_epoch_ns=sequence * 100_000_000,
|
||||
worker_received_monotonic=0.0,
|
||||
)
|
||||
|
||||
|
||||
def test_incremental_media_buffer_is_bounded_and_fail_closed() -> None:
|
||||
source = IncrementalMediaBuffer(1024)
|
||||
source.append(b"a" * 800)
|
||||
with pytest.raises(ShadowRuntimeError, match="hard bound"):
|
||||
source.append(b"b" * 300)
|
||||
snapshot = source.snapshot()
|
||||
assert snapshot["maximum_depth_bytes"] == 800
|
||||
assert snapshot["failed"] is True
|
||||
with pytest.raises(ShadowRuntimeError, match="source failed"):
|
||||
source.read(100)
|
||||
|
||||
|
||||
def test_persistent_decoder_rejects_camera_fragment_gaps_before_decode() -> None:
|
||||
decoder = PersistentFmp4Decoder(on_frame=lambda _frame: None)
|
||||
decoder.start()
|
||||
decoder.feed_init(b"not-a-real-init")
|
||||
decoder.feed_segment(_metadata(1), b"first")
|
||||
with pytest.raises(ShadowRuntimeError, match="sequence gap"):
|
||||
decoder.feed_segment(_metadata(3), b"third")
|
||||
decoder.finish_input()
|
||||
with pytest.raises(ShadowRuntimeError, match="decoder failed"):
|
||||
decoder.join()
|
||||
|
||||
|
||||
def test_persistent_decoder_streams_real_fmp4_epoch() -> None:
|
||||
pytest.importorskip("av")
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
epoch = (
|
||||
repository
|
||||
/ ".runtime"
|
||||
/ "mission-core"
|
||||
/ "evidence"
|
||||
/ "sessions"
|
||||
/ "20260720T065719Z_viewer_live"
|
||||
/ "media"
|
||||
/ "sensor.camera.right"
|
||||
/ "epoch-1"
|
||||
)
|
||||
if not epoch.is_dir():
|
||||
pytest.skip("canonical RAVNOVES00 camera epoch is unavailable")
|
||||
rows = [json.loads(line) for line in (epoch / "index.jsonl").read_text().splitlines()[:10]]
|
||||
decoded = []
|
||||
decoder = PersistentFmp4Decoder(on_frame=decoded.append)
|
||||
decoder.start()
|
||||
decoder.feed_init((epoch / "init.mp4").read_bytes())
|
||||
for row in rows:
|
||||
decoder.feed_segment(
|
||||
CameraFragmentMetadata(
|
||||
ingress_sequence=int(row["sequence"]),
|
||||
source_sequence=int(row["sequence"]),
|
||||
captured_at_epoch_ns=int(row["host_epoch_ns"]),
|
||||
worker_received_monotonic=0.0,
|
||||
),
|
||||
(epoch / row["path"]).read_bytes(),
|
||||
)
|
||||
decoder.finish_input()
|
||||
decoder.join()
|
||||
|
||||
assert len(decoded) == 10
|
||||
assert [frame.metadata.source_sequence for frame in decoded] == list(range(1, 11))
|
||||
assert all(frame.image.shape == (600, 800, 3) for frame in decoded)
|
||||
assert decoder.snapshot()["media"]["depth_bytes"] == 0
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _module() -> object:
|
||||
path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "prepare_e15_worker_package.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("e15_worker_package_test", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_e15_worker_package_is_minimal_hash_addressed_projection(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
|
||||
package = module.build_worker_package(repository / "src", tmp_path)
|
||||
manifest = module.validate_worker_package(package)
|
||||
|
||||
assert package.name == f"e15-worker-package-{manifest['identity_sha256']}"
|
||||
assert manifest["identity"]["classification"] == (
|
||||
"minimal-live-worker-import-projection"
|
||||
)
|
||||
assert len(manifest["artifacts"]) == 11
|
||||
assert not (package / "k1link" / "device_plugins" / "xgrids_k1" / "mqtt").exists()
|
||||
assert "observation" not in (
|
||||
package / "k1link" / "device_plugins" / "xgrids_k1" / "__init__.py"
|
||||
).read_text()
|
||||
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
WORKER_ROOT = (
|
||||
Path(__file__).parents[1] / "experiments" / "perception" / "worker"
|
||||
)
|
||||
PROFILE_PATH = WORKER_ROOT / "e3_k1_camera1_profile.json"
|
||||
|
||||
|
||||
def _worker_module() -> ModuleType:
|
||||
path = WORKER_ROOT / "run_e3_rectified_segmentation.py"
|
||||
sys.path.insert(0, str(WORKER_ROOT))
|
||||
spec = importlib.util.spec_from_file_location("e3_rectified_segmentation", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_e3_profile_is_pinned_to_camera1_kb4_and_exact_model() -> None:
|
||||
worker = _worker_module()
|
||||
|
||||
profile, digest = worker._profile(PROFILE_PATH)
|
||||
|
||||
assert len(digest) == 64
|
||||
assert profile["source"]["calibration_slot"] == "camera_1"
|
||||
assert profile["source"]["distortion_kb4"] == pytest.approx(
|
||||
[
|
||||
-0.023164451386679667,
|
||||
-0.0014974198594105452,
|
||||
-0.001039213149441563,
|
||||
-0.000035237331915978814,
|
||||
]
|
||||
)
|
||||
assert profile["model"]["revision"] == "8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f"
|
||||
assert profile["model"]["files"]["model.safetensors"]["sha256"] == (
|
||||
"c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782"
|
||||
)
|
||||
|
||||
|
||||
def test_kb4_inverse_round_trips_valid_field_angles() -> None:
|
||||
worker = _worker_module()
|
||||
profile, _digest = worker._profile(PROFILE_PATH)
|
||||
coefficients = np.asarray(profile["source"]["distortion_kb4"])
|
||||
theta = np.linspace(0.0, math.radians(96.4), 512)
|
||||
squared = theta * theta
|
||||
distorted = theta * (
|
||||
1.0
|
||||
+ coefficients[0] * squared
|
||||
+ coefficients[1] * squared**2
|
||||
+ coefficients[2] * squared**3
|
||||
+ coefficients[3] * squared**4
|
||||
)
|
||||
|
||||
recovered = worker._invert_kb4(distorted, coefficients)
|
||||
|
||||
assert recovered == pytest.approx(theta, abs=1e-10)
|
||||
|
||||
|
||||
def test_five_view_rectification_covers_the_k1_valid_circle() -> None:
|
||||
worker = _worker_module()
|
||||
profile, _digest = worker._profile(PROFILE_PATH)
|
||||
width, height = profile["source"]["resolution"]
|
||||
_fx, _fy, cx, cy = profile["source"]["intrinsic_fx_fy_cx_cy"]
|
||||
y, x = np.mgrid[:height, :width]
|
||||
valid_mask = np.hypot(x - cx, y - cy) <= 293.0
|
||||
|
||||
maps = worker._rectification_maps(profile, valid_mask)
|
||||
|
||||
assert maps["coverage"]["coverage_fraction"] == 1.0
|
||||
assert maps["coverage"]["covered_pixel_count"] == int(valid_mask.sum())
|
||||
assert maps["coverage"]["overlap_count"]["min"] >= 1
|
||||
assert maps["coverage"]["overlap_count"]["max"] <= 3
|
||||
|
||||
|
||||
def test_tile_fusion_uses_the_selected_view_and_masks_outside_fov() -> None:
|
||||
worker = _worker_module()
|
||||
valid_mask = np.asarray([[True, True], [False, True]])
|
||||
maps = {
|
||||
"selected_tile": np.asarray([[0, 1], [-1, 1]], dtype=np.int8),
|
||||
"tiles": [
|
||||
{
|
||||
"tile_map_x": np.zeros((2, 2), dtype=np.float32),
|
||||
"tile_map_y": np.zeros((2, 2), dtype=np.float32),
|
||||
},
|
||||
{
|
||||
"tile_map_x": np.ones((2, 2), dtype=np.float32),
|
||||
"tile_map_y": np.ones((2, 2), dtype=np.float32),
|
||||
},
|
||||
],
|
||||
}
|
||||
semantics = [
|
||||
np.asarray([[4, 4], [4, 4]], dtype=np.uint8),
|
||||
np.asarray([[7, 7], [7, 7]], dtype=np.uint8),
|
||||
]
|
||||
|
||||
result = worker._fuse_tiles(semantics, maps, valid_mask)
|
||||
|
||||
assert result.tolist() == [[4, 7], [0, 7]]
|
||||
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def _worker_module() -> ModuleType:
|
||||
worker_root = Path(__file__).parents[1] / "experiments" / "perception" / "worker"
|
||||
path = worker_root / "run_e4_full_session_segmentation.py"
|
||||
spec = importlib.util.spec_from_file_location("e4_full_session_worker", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
sys.path.insert(0, str(worker_root))
|
||||
try:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
finally:
|
||||
sys.path.remove(str(worker_root))
|
||||
return module
|
||||
|
||||
|
||||
def _profile() -> dict[str, object]:
|
||||
path = (
|
||||
Path(__file__).parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e3_k1_camera1_profile.json"
|
||||
)
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert isinstance(value, dict)
|
||||
return value
|
||||
|
||||
|
||||
def test_e4_accepts_the_sealed_camera1_valid_fov() -> None:
|
||||
worker = _worker_module()
|
||||
root = (
|
||||
Path(__file__).parents[1]
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e1"
|
||||
/ "valid-fov"
|
||||
/ "valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2"
|
||||
)
|
||||
if not root.is_dir():
|
||||
pytest.skip("real sealed LAB E1 valid-FOV artifact is unavailable")
|
||||
|
||||
mask, identity = worker._load_valid_fov(
|
||||
root,
|
||||
{"input": {"source_id": "sensor.camera.right"}},
|
||||
_profile(),
|
||||
)
|
||||
|
||||
assert mask.shape == (600, 800)
|
||||
assert mask.dtype == np.bool_
|
||||
assert int(mask.sum()) == 270_606
|
||||
assert identity["valid_pixel_count"] == 270_606
|
||||
|
||||
|
||||
def test_e4_rejects_valid_fov_from_another_camera() -> None:
|
||||
worker = _worker_module()
|
||||
root = (
|
||||
Path(__file__).parents[1]
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e1"
|
||||
/ "valid-fov"
|
||||
/ "valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2"
|
||||
)
|
||||
if not root.is_dir():
|
||||
pytest.skip("real sealed LAB E1 valid-FOV artifact is unavailable")
|
||||
|
||||
with pytest.raises(RuntimeError, match="valid-FOV binding"):
|
||||
worker._load_valid_fov(
|
||||
root,
|
||||
{"input": {"source_id": "sensor.camera.left"}},
|
||||
_profile(),
|
||||
)
|
||||
|
||||
|
||||
def test_e4_overlay_preserves_pixels_outside_semantic_mask() -> None:
|
||||
worker = _worker_module()
|
||||
image = np.full((2, 3, 3), 100, dtype=np.uint8)
|
||||
semantic = np.asarray([[0, 4, 0], [7, 0, 1]], dtype=np.uint8)
|
||||
|
||||
overlay = worker._overlay(image, semantic)
|
||||
|
||||
assert np.array_equal(overlay[semantic == 0], image[semantic == 0])
|
||||
assert np.all(np.any(overlay[semantic > 0] != image[semantic > 0], axis=1))
|
||||
|
||||
|
||||
def test_e4_class_fractions_use_only_valid_fov_pixels() -> None:
|
||||
worker = _worker_module()
|
||||
semantic = np.asarray([[0, 4, 4], [7, 0, 1]], dtype=np.uint8)
|
||||
valid = np.asarray([[False, True, True], [True, False, True]])
|
||||
names = {index: f"class-{index}" for index in range(16)}
|
||||
|
||||
classes = worker._class_document(semantic, valid, names)
|
||||
|
||||
assert {item["id"]: item["pixels"] for item in classes} == {1: 1, 4: 2, 7: 1}
|
||||
assert sum(float(item["fraction_of_valid_fov"]) for item in classes) == 1.0
|
||||
@@ -0,0 +1,160 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def _worker_module() -> ModuleType:
|
||||
worker_root = Path(__file__).parents[1] / "experiments" / "perception" / "worker"
|
||||
path = worker_root / "run_e5_instance_tracking.py"
|
||||
sys.path.insert(0, str(worker_root))
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("e5_instance_tracking_worker", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def _profile_path() -> Path:
|
||||
return (
|
||||
Path(__file__).parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e5_yolox_bytetrack_profile.json"
|
||||
)
|
||||
|
||||
|
||||
def _detection(*, score: float, x: float = 100, class_id: int = 0) -> dict[str, object]:
|
||||
return {
|
||||
"class_id": class_id,
|
||||
"label": "person" if class_id == 0 else "car",
|
||||
"score": score,
|
||||
"bbox_xyxy": [x, 100.0, x + 60.0, 220.0],
|
||||
"valid_fov_fraction": 1.0,
|
||||
}
|
||||
|
||||
|
||||
def test_profile_is_pinned_and_semantically_valid() -> None:
|
||||
worker = _worker_module()
|
||||
|
||||
profile, digest = worker._read_profile(_profile_path())
|
||||
|
||||
assert len(digest) == 64
|
||||
assert profile["model"]["id"] == "yolox_s"
|
||||
assert profile["model"]["model_sha256"] == (
|
||||
"c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063"
|
||||
)
|
||||
assert profile["tracking"]["algorithm"] == "bytetrack-style-two-stage-iou/v1"
|
||||
assert profile["detection"]["target_class_ids"] == [0, 1, 2, 3, 5, 7]
|
||||
|
||||
|
||||
def test_profile_rejects_duplicate_target_classes(tmp_path: Path) -> None:
|
||||
worker = _worker_module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
profile["detection"]["target_class_ids"] = [0, 0]
|
||||
path = tmp_path / "profile.json"
|
||||
path.write_text(json.dumps(profile), encoding="utf-8")
|
||||
|
||||
with pytest.raises(RuntimeError, match="target class"):
|
||||
worker._read_profile(path)
|
||||
|
||||
|
||||
def test_valid_fraction_uses_box_area_and_center_gate() -> None:
|
||||
worker = _worker_module()
|
||||
mask = np.zeros((10, 10), dtype=bool)
|
||||
mask[2:8, 2:8] = True
|
||||
integral = np.pad(mask.astype(np.int64), ((1, 0), (1, 0))).cumsum(0).cumsum(1)
|
||||
|
||||
fraction, center_inside, area = worker._valid_fraction(
|
||||
np.asarray([1.0, 1.0, 5.0, 5.0]), integral
|
||||
)
|
||||
|
||||
assert area == 16
|
||||
assert fraction == pytest.approx(9 / 16)
|
||||
assert center_inside is True
|
||||
|
||||
|
||||
def test_yolox_decode_and_fov_filter_admit_one_person() -> None:
|
||||
worker = _worker_module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
output = np.zeros((1, 8400, 85), dtype=np.float32)
|
||||
output[0, 0, :4] = [40.0, 30.0, math.log(10.0), math.log(10.0)]
|
||||
output[0, 0, 4] = 0.9
|
||||
output[0, 0, 5] = 0.9
|
||||
|
||||
detections, rejected = worker._detections(
|
||||
output,
|
||||
profile,
|
||||
np.ones((600, 800), dtype=bool),
|
||||
)
|
||||
|
||||
assert rejected == {}
|
||||
assert len(detections) == 1
|
||||
assert detections[0]["label"] == "person"
|
||||
assert detections[0]["score"] == pytest.approx(0.81)
|
||||
assert detections[0]["bbox_xyxy"] == pytest.approx([350.0, 250.0, 450.0, 350.0])
|
||||
|
||||
|
||||
def test_nms_suppresses_lower_score_box_contained_inside_object() -> None:
|
||||
worker = _worker_module()
|
||||
boxes = np.asarray(
|
||||
[
|
||||
[0.0, 0.0, 100.0, 100.0],
|
||||
[10.0, 10.0, 60.0, 60.0],
|
||||
]
|
||||
)
|
||||
scores = np.asarray([0.9, 0.5])
|
||||
|
||||
assert worker._nms(boxes, scores, 0.45, 0.8) == [0]
|
||||
|
||||
|
||||
def test_two_stage_tracker_keeps_id_through_low_score_detection() -> None:
|
||||
worker = _worker_module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
tracker = worker.TwoStageTracker(profile["tracking"])
|
||||
|
||||
assert tracker.update([_detection(score=0.8)], 0) == []
|
||||
second = tracker.update([_detection(score=0.7, x=103)], 1)
|
||||
third = tracker.update([_detection(score=0.15, x=106)], 2)
|
||||
|
||||
assert [track.track_id for track in second] == [1]
|
||||
assert [track.track_id for track in third] == [1]
|
||||
assert third[0].hits == 3
|
||||
assert tracker.created == 1
|
||||
|
||||
|
||||
def test_tracker_does_not_cross_match_classes() -> None:
|
||||
worker = _worker_module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
tracker = worker.TwoStageTracker(profile["tracking"])
|
||||
|
||||
tracker.update([_detection(score=0.8)], 0)
|
||||
tracks = tracker.update([_detection(score=0.8, class_id=2)], 1)
|
||||
|
||||
assert tracks == []
|
||||
assert tracker.created == 2
|
||||
assert {track.class_id for track in tracker.tracks} == {0, 2}
|
||||
|
||||
|
||||
def test_clip_timeline_may_start_at_nonzero_source_index(tmp_path: Path) -> None:
|
||||
worker = _worker_module()
|
||||
path = tmp_path / "timeline.jsonl"
|
||||
rows = [
|
||||
{"frame_index": 0, "source_frame_index": 1000, "session_seconds": 135.1},
|
||||
{"frame_index": 1, "source_frame_index": 1001, "session_seconds": 135.2},
|
||||
]
|
||||
path.write_text("\n".join(json.dumps(row) for row in rows) + "\n", encoding="utf-8")
|
||||
|
||||
assert worker._read_timeline(path, 2) == rows
|
||||
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict, deque
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _module() -> ModuleType:
|
||||
path = (
|
||||
Path(__file__).parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "fuse_e6_tracking_lidar.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("e6_tracking_lidar", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _profile_path() -> Path:
|
||||
return (
|
||||
Path(__file__).parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e6_tracking_lidar_profile.json"
|
||||
)
|
||||
|
||||
|
||||
def test_profile_pins_e4_e5_and_tight_sync_gate() -> None:
|
||||
module = _module()
|
||||
|
||||
profile, digest = module._read_profile(_profile_path())
|
||||
|
||||
assert len(digest) == 64
|
||||
assert profile["inputs"]["tracking_result_id"].startswith("e5-tracking-")
|
||||
assert profile["inputs"]["semantic_result_id"].startswith("result-")
|
||||
assert profile["temporal"]["maximum_lidar_camera_delta_ms"] == 100.0
|
||||
assert profile["projection"]["occlusion_policy"] == (
|
||||
"nearest-depth-per-rounded-pixel"
|
||||
)
|
||||
|
||||
|
||||
def test_profile_rejects_unsafe_sync_window(tmp_path: Path) -> None:
|
||||
module = _module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
profile["temporal"]["maximum_lidar_camera_delta_ms"] = 500.0
|
||||
path = tmp_path / "profile.json"
|
||||
path.write_text(json.dumps(profile), encoding="utf-8")
|
||||
|
||||
try:
|
||||
module._read_profile(path)
|
||||
except RuntimeError as exc:
|
||||
assert "contract" in str(exc)
|
||||
else:
|
||||
raise AssertionError("unsafe synchronization window was accepted")
|
||||
|
||||
|
||||
def test_depth_buffer_keeps_nearest_point_per_rounded_pixel() -> None:
|
||||
module = _module()
|
||||
pixels = np.asarray([[10.2, 20.2], [10.4, 20.4], [12.0, 20.0]])
|
||||
depths = np.asarray([7.0, 3.0, 5.0])
|
||||
|
||||
selected = module._visible_projection_indices(pixels, depths)
|
||||
|
||||
assert selected.tolist() == [1, 2]
|
||||
|
||||
|
||||
def test_depth_cluster_prefers_largest_contiguous_group() -> None:
|
||||
module = _module()
|
||||
depths = np.asarray([2.0, 2.1, 8.0, 8.1, 8.2])
|
||||
|
||||
selected = module._depth_cluster(
|
||||
np.arange(5, dtype=np.int64),
|
||||
depths,
|
||||
minimum_gap_m=0.5,
|
||||
gap_fraction=0.06,
|
||||
)
|
||||
|
||||
assert selected.tolist() == [2, 3, 4]
|
||||
|
||||
|
||||
def test_box_candidates_apply_inner_roi() -> None:
|
||||
module = _module()
|
||||
pixels = np.asarray([[0.0, 0.0], [5.0, 5.0], [50.0, 50.0], [99.0, 99.0]])
|
||||
inset = {
|
||||
"horizontal_fraction": 0.05,
|
||||
"top_fraction": 0.04,
|
||||
"bottom_fraction": 0.02,
|
||||
}
|
||||
|
||||
selected = module._box_candidates((0.0, 0.0, 100.0, 100.0), pixels, inset)
|
||||
|
||||
assert selected.tolist() == [1, 2]
|
||||
|
||||
|
||||
def test_vehicle_group_nms_suppresses_overlapping_car_truck_tracks() -> None:
|
||||
module = _module()
|
||||
tracks = [
|
||||
{
|
||||
"track_id": 1,
|
||||
"label": "car",
|
||||
"score": 0.9,
|
||||
"bbox_xyxy": [0.0, 0.0, 100.0, 100.0],
|
||||
},
|
||||
{
|
||||
"track_id": 2,
|
||||
"label": "truck",
|
||||
"score": 0.4,
|
||||
"bbox_xyxy": [2.0, 2.0, 102.0, 102.0],
|
||||
},
|
||||
{
|
||||
"track_id": 3,
|
||||
"label": "person",
|
||||
"score": 0.8,
|
||||
"bbox_xyxy": [2.0, 2.0, 102.0, 102.0],
|
||||
},
|
||||
]
|
||||
|
||||
selected = module._suppress_duplicate_vehicle_tracks(
|
||||
tracks,
|
||||
vehicle_labels={"car", "truck", "bus"},
|
||||
iou_threshold=0.55,
|
||||
)
|
||||
|
||||
assert [item["track_id"] for item in selected] == [1, 3]
|
||||
|
||||
|
||||
def test_spatial_cluster_separates_distant_same_depth_surfaces() -> None:
|
||||
module = _module()
|
||||
points = np.asarray(
|
||||
[[0.0, 0.0, 0.0], [0.2, 0.1, 0.1], [5.0, 0.0, 0.0], [5.2, 0.1, 0.0]]
|
||||
)
|
||||
|
||||
selected = module._spatial_cluster(
|
||||
np.arange(4, dtype=np.int64),
|
||||
points,
|
||||
radius_m=0.5,
|
||||
)
|
||||
|
||||
assert selected.size == 2
|
||||
assert set(selected.tolist()) in ({0, 1}, {2, 3})
|
||||
|
||||
|
||||
def test_semantic_supported_track_produces_point_cuboid_and_distance() -> None:
|
||||
module = _module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
semantic = np.zeros((600, 800), dtype=np.uint8)
|
||||
semantic[100:200, 100:200] = 1
|
||||
pixels = np.asarray([[120.0, 120.0], [130.0, 130.0], [140.0, 140.0]])
|
||||
depths = np.asarray([5.0, 5.1, 5.2])
|
||||
points = np.asarray(
|
||||
[[1.0, 2.0, 0.2], [1.2, 2.1, 0.8], [1.4, 2.2, 1.6]],
|
||||
dtype=np.float64,
|
||||
)
|
||||
tracks = [
|
||||
{
|
||||
"track_id": 7,
|
||||
"label": "person",
|
||||
"score": 0.9,
|
||||
"bbox_xyxy": [100.0, 100.0, 200.0, 200.0],
|
||||
}
|
||||
]
|
||||
|
||||
result = module._fuse_tracks(
|
||||
tracks=tracks,
|
||||
semantic_map=semantic,
|
||||
pixels_xy=pixels,
|
||||
depths_m=depths,
|
||||
projected_source_indices=np.arange(3, dtype=np.int64),
|
||||
points_map=points,
|
||||
points_lidar=points,
|
||||
profile=profile,
|
||||
distance_history=defaultdict(lambda: deque(maxlen=5)),
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].cuboid is not None
|
||||
assert result[0].clustered_points == 3
|
||||
assert result[0].distance_median_m is not None
|
||||
assert result[0].distance_smoothed_m == result[0].distance_median_m
|
||||
|
||||
|
||||
def test_semantic_mismatch_never_extrudes_rectangle_into_cuboid() -> None:
|
||||
module = _module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
pixels = np.asarray([[120.0, 120.0], [130.0, 130.0], [140.0, 140.0]])
|
||||
points = np.asarray(
|
||||
[[1.0, 2.0, 0.2], [1.2, 2.1, 0.8], [1.4, 2.2, 1.6]],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
result = module._fuse_tracks(
|
||||
tracks=[
|
||||
{
|
||||
"track_id": 7,
|
||||
"label": "person",
|
||||
"score": 0.9,
|
||||
"bbox_xyxy": [100.0, 100.0, 200.0, 200.0],
|
||||
}
|
||||
],
|
||||
semantic_map=np.full((600, 800), 4, dtype=np.uint8),
|
||||
pixels_xy=pixels,
|
||||
depths_m=np.asarray([5.0, 5.1, 5.2]),
|
||||
projected_source_indices=np.arange(3, dtype=np.int64),
|
||||
points_map=points,
|
||||
points_lidar=points,
|
||||
profile=profile,
|
||||
distance_history=defaultdict(lambda: deque(maxlen=5)),
|
||||
)
|
||||
|
||||
assert result[0].cuboid is None
|
||||
assert result[0].status == "rejected-no-semantic-lidar-support"
|
||||
|
||||
|
||||
def test_distance_innovation_rejects_background_jump_for_same_track() -> None:
|
||||
module = _module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
semantic = np.zeros((600, 800), dtype=np.uint8)
|
||||
semantic[100:200, 100:200] = 1
|
||||
pixels = np.asarray([[120.0, 120.0], [130.0, 130.0], [140.0, 140.0]])
|
||||
tracks = [
|
||||
{
|
||||
"track_id": 7,
|
||||
"label": "person",
|
||||
"score": 0.9,
|
||||
"bbox_xyxy": [100.0, 100.0, 200.0, 200.0],
|
||||
}
|
||||
]
|
||||
history = defaultdict(lambda: deque(maxlen=5))
|
||||
near = np.asarray(
|
||||
[[2.0, 0.0, 0.2], [2.1, 0.1, 0.8], [2.2, 0.2, 1.5]],
|
||||
dtype=np.float64,
|
||||
)
|
||||
far = near + np.asarray([6.0, 0.0, 0.0])
|
||||
|
||||
first = module._fuse_tracks(
|
||||
tracks=tracks,
|
||||
semantic_map=semantic,
|
||||
pixels_xy=pixels,
|
||||
depths_m=np.asarray([2.0, 2.1, 2.2]),
|
||||
projected_source_indices=np.arange(3, dtype=np.int64),
|
||||
points_map=near,
|
||||
points_lidar=near,
|
||||
profile=profile,
|
||||
distance_history=history,
|
||||
)
|
||||
second = module._fuse_tracks(
|
||||
tracks=tracks,
|
||||
semantic_map=semantic,
|
||||
pixels_xy=pixels,
|
||||
depths_m=np.asarray([8.0, 8.1, 8.2]),
|
||||
projected_source_indices=np.arange(3, dtype=np.int64),
|
||||
points_map=far,
|
||||
points_lidar=far,
|
||||
profile=profile,
|
||||
distance_history=history,
|
||||
)
|
||||
|
||||
assert first[0].cuboid is not None
|
||||
assert second[0].cuboid is None
|
||||
assert second[0].status == "rejected-distance-innovation"
|
||||
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def _worker_module() -> ModuleType:
|
||||
worker_root = Path(__file__).parents[1] / "experiments" / "perception" / "worker"
|
||||
path = worker_root / "run_e8_realtime_tracking.py"
|
||||
sys.path.insert(0, str(worker_root))
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("e8_realtime_tracking_worker", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def _profile_path(name: str = "e8_realtime_tracking_profile.json") -> Path:
|
||||
return Path(__file__).parents[1] / "experiments" / "perception" / "worker" / name
|
||||
|
||||
|
||||
def _envelope(worker: ModuleType, index: int) -> object:
|
||||
return worker.FrameEnvelope(
|
||||
frame_index=index,
|
||||
path=Path(f"frame-{index:06d}.png"),
|
||||
timeline={"source_frame_index": 1000 + index},
|
||||
scheduled_monotonic=float(index),
|
||||
decoded_monotonic=float(index),
|
||||
image=np.zeros((1, 1, 3), dtype=np.uint8),
|
||||
decode_ms=1.0,
|
||||
source_release_lag_ms=0.0,
|
||||
)
|
||||
|
||||
|
||||
def test_e8_profile_pins_realtime_policy_and_e5_model() -> None:
|
||||
worker = _worker_module()
|
||||
|
||||
profile, digest = worker._read_profile(_profile_path())
|
||||
|
||||
assert len(digest) == 64
|
||||
assert profile["model"]["id"] == "yolox_s"
|
||||
assert profile["realtime"]["queue_policy"] == "bounded-latest-wins"
|
||||
assert profile["realtime"]["queue_capacity"] == 2
|
||||
assert profile["acceptance"]["minimum_effective_fps"] == 9.5
|
||||
assert profile["acceptance"]["expect_overload"] is False
|
||||
|
||||
|
||||
def test_e8_overload_profile_requires_observed_overload() -> None:
|
||||
worker = _worker_module()
|
||||
|
||||
profile, _digest = worker._read_profile(
|
||||
_profile_path("e8_realtime_tracking_overload_profile.json")
|
||||
)
|
||||
|
||||
assert profile["mode"] == "overload-negative-control"
|
||||
assert profile["realtime"]["consumer_delay_ms"] == 140.0
|
||||
assert profile["acceptance"]["expect_overload"] is True
|
||||
|
||||
|
||||
def test_e8_profile_rejects_unbounded_queue(tmp_path: Path) -> None:
|
||||
worker = _worker_module()
|
||||
profile = json.loads(_profile_path().read_text(encoding="utf-8"))
|
||||
profile["realtime"]["queue_capacity"] = 0
|
||||
path = tmp_path / "profile.json"
|
||||
path.write_text(json.dumps(profile), encoding="utf-8")
|
||||
|
||||
with pytest.raises(RuntimeError, match="scheduling"):
|
||||
worker._read_profile(path)
|
||||
|
||||
|
||||
def test_e8_latest_wins_queue_discards_oldest_frame() -> None:
|
||||
worker = _worker_module()
|
||||
queue = worker.LatestWinsQueue(capacity=2)
|
||||
queue.publish(_envelope(worker, 1))
|
||||
queue.publish(_envelope(worker, 2))
|
||||
queue.publish(_envelope(worker, 3))
|
||||
queue.close()
|
||||
|
||||
assert queue.take().frame_index == 2
|
||||
assert queue.take().frame_index == 3
|
||||
assert queue.take() is None
|
||||
assert queue.snapshot() == {
|
||||
"capacity": 2,
|
||||
"final_depth": 0,
|
||||
"maximum_depth": 2,
|
||||
"published": 3,
|
||||
"consumed": 2,
|
||||
"dropped_overflow": 1,
|
||||
"closed": True,
|
||||
}
|
||||
|
||||
|
||||
def test_e8_health_uses_explicit_freshness_thresholds() -> None:
|
||||
worker = _worker_module()
|
||||
realtime = {"stale_after_ms": 150.0, "unavailable_after_ms": 500.0}
|
||||
|
||||
assert worker._health(149.9, realtime) == "healthy"
|
||||
assert worker._health(150.0, realtime) == "stale"
|
||||
assert worker._health(500.0, realtime) == "unavailable"
|
||||
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _worker_module() -> ModuleType:
|
||||
worker_root = Path(__file__).parents[1] / "experiments" / "perception" / "worker"
|
||||
path = worker_root / "run_e9_multirate_perception.py"
|
||||
sys.path.insert(0, str(worker_root))
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("e9_multirate_worker", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def _profile_path() -> Path:
|
||||
return (
|
||||
Path(__file__).parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "e9_multirate_perception_profile.json"
|
||||
)
|
||||
|
||||
|
||||
def _semantic(worker: ModuleType, *, index: int, session_seconds: float) -> object:
|
||||
return worker.SemanticResult(
|
||||
frame_index=index,
|
||||
source_frame_index=1000 + index,
|
||||
session_seconds=session_seconds,
|
||||
completed_monotonic=10.0,
|
||||
completion_age_ms=180.0,
|
||||
mask_sha256="a" * 64,
|
||||
class_pixels={"road": 100},
|
||||
class_fractions={"road": 0.5},
|
||||
)
|
||||
|
||||
|
||||
def test_e9_profile_pins_independent_bounded_rates() -> None:
|
||||
worker = _worker_module()
|
||||
|
||||
profile, digest = worker._read_profile(_profile_path())
|
||||
|
||||
assert len(digest) == 64
|
||||
assert profile["replay"]["detector_queue_capacity"] == 2
|
||||
assert profile["replay"]["semantic_queue_capacity"] == 1
|
||||
assert profile["replay"]["semantic_sample_every_frames"] == 5
|
||||
assert profile["replay"]["semantic_ttl_ms"] == 750.0
|
||||
|
||||
|
||||
def test_e9_semantic_binding_exposes_fresh_stale_and_unavailable() -> None:
|
||||
worker = _worker_module()
|
||||
semantic = _semantic(worker, index=5, session_seconds=20.0)
|
||||
|
||||
assert (
|
||||
worker._semantic_binding(
|
||||
None,
|
||||
frame_session_seconds=20.1,
|
||||
ttl_ms=750.0,
|
||||
)["status"]
|
||||
== "unavailable"
|
||||
)
|
||||
assert (
|
||||
worker._semantic_binding(
|
||||
semantic,
|
||||
frame_session_seconds=20.7,
|
||||
ttl_ms=750.0,
|
||||
)["status"]
|
||||
== "fresh"
|
||||
)
|
||||
assert (
|
||||
worker._semantic_binding(
|
||||
semantic,
|
||||
frame_session_seconds=20.8,
|
||||
ttl_ms=750.0,
|
||||
)["status"]
|
||||
== "stale"
|
||||
)
|
||||
|
||||
|
||||
def test_e9_latest_semantic_rejects_time_reversal() -> None:
|
||||
worker = _worker_module()
|
||||
latest = worker.LatestSemantic()
|
||||
latest.publish(_semantic(worker, index=5, session_seconds=20.0))
|
||||
|
||||
with pytest.raises(RuntimeError, match="not monotonic"):
|
||||
latest.publish(_semantic(worker, index=4, session_seconds=19.9))
|
||||
@@ -67,3 +67,20 @@ def test_metrics_distinguish_preview_drops_and_pipeline_latency() -> None:
|
||||
assert snapshot["last_point_count"] == 42
|
||||
assert snapshot["mqtt_to_publish_ms"] == 12.346
|
||||
assert snapshot["decode_publish_ms"] == 1.234
|
||||
|
||||
|
||||
def test_metrics_report_live_ai_latency_rate_staleness_and_drops() -> None:
|
||||
metrics = BridgeMetrics()
|
||||
metrics.perception_dropped()
|
||||
metrics.published_perception(
|
||||
captured_at_epoch_ns=1_000_000_000,
|
||||
published_at_epoch_ns=1_125_000_000,
|
||||
published_monotonic_ns=2_000_000_000,
|
||||
)
|
||||
|
||||
snapshot = metrics.snapshot()
|
||||
|
||||
assert snapshot["perception_frames"] == 1
|
||||
assert snapshot["perception_dropped"] == 1
|
||||
assert snapshot["perception_end_to_end_ms"] == 125.0
|
||||
assert snapshot["perception_end_to_end_p95_ms"] == 125.0
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.compute.live_perception import (
|
||||
LIVE_INGRESS_WIRE_SCHEMA,
|
||||
LatestWinsQueue,
|
||||
LivePerceptionIngress,
|
||||
WorldStateProjector,
|
||||
classify_health,
|
||||
decode_live_perception_result,
|
||||
encode_live_perception_result,
|
||||
)
|
||||
|
||||
|
||||
def test_live_ingress_keeps_modalities_separately_bounded_and_ordered() -> None:
|
||||
ingress = LivePerceptionIngress()
|
||||
ingress.open_consumer("worker-1")
|
||||
ingress.begin_session("session-1")
|
||||
|
||||
for sequence in range(5):
|
||||
assert ingress.publish(
|
||||
modality="camera-frame",
|
||||
source_id="sensor.camera.right",
|
||||
source_sequence=sequence,
|
||||
captured_at_epoch_ns=100 + sequence,
|
||||
received_monotonic_ns=200 + sequence,
|
||||
payload=f"camera-{sequence}".encode(),
|
||||
)
|
||||
for sequence in range(3):
|
||||
assert ingress.publish(
|
||||
modality="lidar",
|
||||
source_id="lixel/application/report/lio_pcl",
|
||||
source_sequence=sequence,
|
||||
captured_at_epoch_ns=300 + sequence,
|
||||
received_monotonic_ns=400 + sequence,
|
||||
payload=f"lidar-{sequence}".encode(),
|
||||
)
|
||||
for sequence in range(20):
|
||||
assert ingress.publish(
|
||||
modality="pose",
|
||||
source_id="lixel/application/report/lio_pose",
|
||||
source_sequence=sequence,
|
||||
captured_at_epoch_ns=500 + sequence,
|
||||
received_monotonic_ns=600 + sequence,
|
||||
payload=f"pose-{sequence}".encode(),
|
||||
)
|
||||
|
||||
snapshot = ingress.snapshot()
|
||||
assert snapshot["queues"]["camera-frame"]["depth"] == 2
|
||||
assert snapshot["queues"]["camera-frame"]["dropped_overflow"] == 3
|
||||
assert snapshot["queues"]["lidar"]["depth"] == 2
|
||||
assert snapshot["queues"]["lidar"]["dropped_overflow"] == 1
|
||||
assert snapshot["queues"]["pose"]["depth"] == 16
|
||||
assert snapshot["queues"]["pose"]["dropped_overflow"] == 4
|
||||
|
||||
events = []
|
||||
while (event := ingress.take_next("worker-1", timeout=0)) is not None:
|
||||
events.append(event)
|
||||
assert events[0].modality == "control"
|
||||
assert [event.ingress_sequence for event in events] == sorted(
|
||||
event.ingress_sequence for event in events
|
||||
)
|
||||
assert [event.source_sequence for event in events if event.modality == "camera-frame"] == [
|
||||
3,
|
||||
4,
|
||||
]
|
||||
assert [event.source_sequence for event in events if event.modality == "lidar"] == [1, 2]
|
||||
assert [event.source_sequence for event in events if event.modality == "pose"] == list(
|
||||
range(4, 20)
|
||||
)
|
||||
|
||||
|
||||
def test_live_ingress_wire_is_self_delimiting_and_explicitly_non_authoritative() -> None:
|
||||
ingress = LivePerceptionIngress()
|
||||
ingress.open_consumer("worker-1")
|
||||
ingress.begin_session("session-1")
|
||||
assert ingress.publish(
|
||||
modality="camera-init",
|
||||
source_id="sensor.camera.right",
|
||||
source_sequence=0,
|
||||
captured_at_epoch_ns=123,
|
||||
received_monotonic_ns=456,
|
||||
payload=b"init",
|
||||
)
|
||||
ingress.take_next("worker-1", timeout=0)
|
||||
event = ingress.take_next("worker-1", timeout=0)
|
||||
assert event is not None
|
||||
|
||||
encoded = event.wire_bytes()
|
||||
header_bytes = struct.unpack("!I", encoded[:4])[0]
|
||||
header = json.loads(encoded[4 : 4 + header_bytes])
|
||||
assert header["schema_version"] == LIVE_INGRESS_WIRE_SCHEMA
|
||||
assert header["payload_bytes"] == 4
|
||||
assert header["commands_enabled"] is False
|
||||
assert header["navigation_or_safety_accepted"] is False
|
||||
assert encoded[4 + header_bytes :] == b"init"
|
||||
|
||||
|
||||
def test_live_ingress_rejects_oversize_without_affecting_other_modalities() -> None:
|
||||
ingress = LivePerceptionIngress()
|
||||
ingress.begin_session("session-1")
|
||||
assert not ingress.publish(
|
||||
modality="camera-frame",
|
||||
source_id="sensor.camera.right",
|
||||
source_sequence=1,
|
||||
captured_at_epoch_ns=1,
|
||||
received_monotonic_ns=1,
|
||||
payload=b"x" * (1024 * 1024 + 1),
|
||||
)
|
||||
assert ingress.publish(
|
||||
modality="pose",
|
||||
source_id="RealtimePath",
|
||||
source_sequence=2,
|
||||
captured_at_epoch_ns=2,
|
||||
received_monotonic_ns=2,
|
||||
payload=b"pose",
|
||||
)
|
||||
snapshot = ingress.snapshot()
|
||||
assert snapshot["queues"]["camera-frame"]["rejected_oversize"] == 1
|
||||
assert snapshot["queues"]["pose"]["published"] == 1
|
||||
|
||||
|
||||
def test_live_ingress_allows_only_one_worker_consumer() -> None:
|
||||
ingress = LivePerceptionIngress()
|
||||
ingress.open_consumer("worker-1")
|
||||
with pytest.raises(RuntimeError, match="already has a consumer"):
|
||||
ingress.open_consumer("worker-2")
|
||||
ingress.close_consumer("worker-1")
|
||||
ingress.open_consumer("worker-2")
|
||||
|
||||
|
||||
def test_live_result_round_trip_keeps_video_mask_boxes_and_shadow_authority() -> None:
|
||||
mask = np.zeros((600, 800), dtype=np.uint8)
|
||||
mask[100:120, 200:240] = 4
|
||||
encoded = encode_live_perception_result(
|
||||
frame_index=12,
|
||||
source_frame_index=44,
|
||||
session_seconds=1.25,
|
||||
captured_at_epoch_ns=123_000_000,
|
||||
image_jpeg=b"\xff\xd8test\xff\xd9",
|
||||
segmentation_mask=mask,
|
||||
objects=[
|
||||
{
|
||||
"track_id": 7,
|
||||
"label": "car",
|
||||
"score": 0.91,
|
||||
"bbox_xyxy": [10, 20, 110, 80],
|
||||
"cuboid_center_map": [1, 2, 0.5],
|
||||
"cuboid_half_size": [2.25, 0.925, 0.775],
|
||||
"cuboid_quaternion_xyzw": [0, 0, 0, 1],
|
||||
}
|
||||
],
|
||||
delivery={"health": "healthy", "result_age_ms": 49.0},
|
||||
)
|
||||
|
||||
frame = decode_live_perception_result(encoded)
|
||||
|
||||
assert frame.frame_index == 12
|
||||
assert frame.source_frame_index == 44
|
||||
assert frame.image_jpeg == b"\xff\xd8test\xff\xd9"
|
||||
assert frame.segmentation_mask is not None
|
||||
assert np.array_equal(frame.segmentation_mask, mask)
|
||||
assert frame.objects[0]["bbox_xyxy"] == [10.0, 20.0, 110.0, 80.0]
|
||||
assert frame.objects[0]["cuboid_center_map"] == [1.0, 2.0, 0.5]
|
||||
assert frame.delivery["health"] == "healthy"
|
||||
|
||||
|
||||
def test_live_result_rejects_tampering_and_partial_cuboid() -> None:
|
||||
with pytest.raises(ValueError, match="cuboid is incomplete"):
|
||||
encode_live_perception_result(
|
||||
frame_index=0,
|
||||
source_frame_index=0,
|
||||
session_seconds=0.0,
|
||||
captured_at_epoch_ns=1,
|
||||
image_jpeg=b"\xff\xd8x\xff\xd9",
|
||||
segmentation_mask=None,
|
||||
objects=[
|
||||
{
|
||||
"track_id": 1,
|
||||
"label": "car",
|
||||
"score": 0.5,
|
||||
"bbox_xyxy": [0, 0, 1, 1],
|
||||
"cuboid_center_map": [0, 0, 0],
|
||||
}
|
||||
],
|
||||
delivery={"health": "degraded"},
|
||||
)
|
||||
encoded = encode_live_perception_result(
|
||||
frame_index=0,
|
||||
source_frame_index=0,
|
||||
session_seconds=0.0,
|
||||
captured_at_epoch_ns=1,
|
||||
image_jpeg=b"\xff\xd8x\xff\xd9",
|
||||
segmentation_mask=None,
|
||||
objects=[],
|
||||
delivery={"health": "healthy"},
|
||||
)
|
||||
changed = bytearray(encoded)
|
||||
changed[-3] ^= 0x01
|
||||
with pytest.raises(ValueError, match="contract is invalid"):
|
||||
decode_live_perception_result(bytes(changed))
|
||||
|
||||
|
||||
def test_latest_wins_queue_never_exceeds_capacity() -> None:
|
||||
queue = LatestWinsQueue[int](capacity=2)
|
||||
queue.publish(1)
|
||||
queue.publish(2)
|
||||
queue.publish(3)
|
||||
|
||||
assert queue.take_next(timeout=0) == 2
|
||||
assert queue.take_next(timeout=0) == 3
|
||||
snapshot = queue.snapshot()
|
||||
assert snapshot.maximum_depth == 2
|
||||
assert snapshot.depth == 0
|
||||
assert snapshot.dropped_overflow == 1
|
||||
assert snapshot.dropped_superseded == 0
|
||||
assert snapshot.dropped_total == 1
|
||||
|
||||
|
||||
def test_closed_empty_queue_returns_none() -> None:
|
||||
queue = LatestWinsQueue[str](capacity=1)
|
||||
queue.close()
|
||||
|
||||
assert queue.take_next(timeout=0) is None
|
||||
assert queue.snapshot().closed is True
|
||||
|
||||
|
||||
def test_health_distinguishes_depth_degradation_from_staleness() -> None:
|
||||
assert classify_health(
|
||||
source_available=True,
|
||||
fusion_state="fused",
|
||||
result_age_ms=25.0,
|
||||
stale_after_ms=300.0,
|
||||
unavailable_after_ms=1000.0,
|
||||
) == ("healthy", ())
|
||||
assert classify_health(
|
||||
source_available=True,
|
||||
fusion_state="depth-unavailable-sync-gate",
|
||||
result_age_ms=25.0,
|
||||
stale_after_ms=300.0,
|
||||
unavailable_after_ms=1000.0,
|
||||
) == ("degraded", ("depth-unavailable-sync-gate",))
|
||||
assert classify_health(
|
||||
source_available=True,
|
||||
fusion_state="fused",
|
||||
result_age_ms=350.0,
|
||||
stale_after_ms=300.0,
|
||||
unavailable_after_ms=1000.0,
|
||||
) == ("stale", ("result-age-exceeded",))
|
||||
assert classify_health(
|
||||
source_available=False,
|
||||
fusion_state="fused",
|
||||
result_age_ms=0.0,
|
||||
stale_after_ms=300.0,
|
||||
unavailable_after_ms=1000.0,
|
||||
) == ("unavailable", ("source-unavailable",))
|
||||
|
||||
|
||||
def test_world_state_contains_metric_position_size_range_and_velocity() -> None:
|
||||
projector = WorldStateProjector(velocity_history_limit_s=1.0)
|
||||
accepted = {
|
||||
"track_id": 7,
|
||||
"label": "car",
|
||||
"association_group": "vehicle",
|
||||
"score": 0.8,
|
||||
"clustered_points": 12,
|
||||
"distance_smoothed_m": 5.0,
|
||||
"cuboid_status": "accepted-point-supported-oriented-p05-p95",
|
||||
"cuboid_center_map": [1.0, 2.0, 0.5],
|
||||
"cuboid_half_size": [2.0, 1.0, 0.75],
|
||||
"cuboid_quaternion_xyzw": [0.0, 0.0, 0.0, 1.0],
|
||||
}
|
||||
base = {
|
||||
"frame_index": 0,
|
||||
"source_frame_index": 1000,
|
||||
"session_seconds": 10.0,
|
||||
"state": "fused",
|
||||
"objects": [
|
||||
accepted,
|
||||
{**accepted, "track_id": 9, "cuboid_status": "rejected-distance-innovation"},
|
||||
],
|
||||
}
|
||||
first = projector.project(
|
||||
frame=base,
|
||||
lidar_positions={7: [4.0, 0.0, 0.0]},
|
||||
clearance={"front_m": 4.0},
|
||||
)
|
||||
states = [first]
|
||||
for index, elapsed in enumerate((0.2, 0.4, 0.6), start=1):
|
||||
states.append(
|
||||
projector.project(
|
||||
frame={
|
||||
**base,
|
||||
"frame_index": index,
|
||||
"session_seconds": 10.0 + elapsed,
|
||||
"objects": [
|
||||
{
|
||||
**accepted,
|
||||
"cuboid_center_map": [1.0 + 2.0 * elapsed, 2.0, 0.5],
|
||||
}
|
||||
],
|
||||
},
|
||||
lidar_positions={7: [3.0, 0.0, 0.0]},
|
||||
clearance={"front_m": 3.0},
|
||||
)
|
||||
)
|
||||
second = states[-1]
|
||||
|
||||
assert first["object_count"] == 1
|
||||
assert first["objects"][0]["size_m"] == [4.0, 2.0, 1.5]
|
||||
assert first["objects"][0]["position_lidar_m"] == [4.0, 0.0, 0.0]
|
||||
assert first["objects"][0]["velocity_map_mps"] is None
|
||||
assert second["objects"][0]["velocity_map_mps"] == pytest.approx([2.0, 0.0, 0.0])
|
||||
assert second["objects"][0]["speed_mps"] == pytest.approx(2.0)
|
||||
assert second["objects"][0]["velocity_status"] == "diagnostic-robust-history"
|
||||
|
||||
|
||||
def test_world_state_declares_missing_vehicle_body_transform() -> None:
|
||||
state = WorldStateProjector().project(
|
||||
frame={
|
||||
"frame_index": 0,
|
||||
"source_frame_index": 1,
|
||||
"session_seconds": 1.0,
|
||||
"state": "depth-unavailable-sync-gate",
|
||||
"objects": [],
|
||||
},
|
||||
lidar_positions={},
|
||||
clearance={"state": "unavailable"},
|
||||
)
|
||||
|
||||
assert state["coordinate_frames"]["sensor_relative"] == "k1-lidar"
|
||||
assert state["coordinate_frames"]["vehicle_body"].startswith("unavailable")
|
||||
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import stat
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.compute.live_perception import (
|
||||
LivePerceptionIngress,
|
||||
encode_live_perception_result,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.live_perception_shadow import (
|
||||
build_live_perception_shadow_router,
|
||||
ensure_live_shadow_token,
|
||||
)
|
||||
|
||||
|
||||
def test_shadow_token_is_stable_and_private(tmp_path: Path) -> None:
|
||||
path, token = ensure_live_shadow_token(tmp_path)
|
||||
repeated_path, repeated_token = ensure_live_shadow_token(tmp_path)
|
||||
|
||||
assert repeated_path == path
|
||||
assert repeated_token == token
|
||||
assert stat.S_IMODE(path.stat().st_mode) == 0o600
|
||||
assert stat.S_IMODE(path.parent.stat().st_mode) == 0o700
|
||||
|
||||
|
||||
def test_shadow_router_exposes_only_the_exclusive_binary_stream() -> None:
|
||||
ingress = LivePerceptionIngress()
|
||||
router = build_live_perception_shadow_router(
|
||||
ingress,
|
||||
"test-plugin",
|
||||
bearer_token="x" * 43,
|
||||
)
|
||||
assert len(router.routes) == 1
|
||||
assert router.routes[0].path == (
|
||||
"/api/v1/device-plugins/test-plugin/live-perception-shadow"
|
||||
)
|
||||
|
||||
|
||||
def test_shadow_router_accepts_only_validated_diagnostic_results_back() -> None:
|
||||
ingress = LivePerceptionIngress()
|
||||
received: list[bytes] = []
|
||||
published = threading.Event()
|
||||
|
||||
def receive_result(payload: bytes) -> bool:
|
||||
received.append(payload)
|
||||
published.set()
|
||||
return True
|
||||
|
||||
router = build_live_perception_shadow_router(
|
||||
ingress,
|
||||
"test-plugin",
|
||||
bearer_token="x" * 43,
|
||||
result_receiver=receive_result,
|
||||
)
|
||||
ingress.begin_session("session-1")
|
||||
encoded = encode_live_perception_result(
|
||||
frame_index=0,
|
||||
source_frame_index=0,
|
||||
session_seconds=0.0,
|
||||
captured_at_epoch_ns=1,
|
||||
image_jpeg=bytes.fromhex("ffd878ffd9"),
|
||||
segmentation_mask=np.zeros((600, 800), dtype=np.uint8),
|
||||
objects=[],
|
||||
delivery={"health": "healthy"},
|
||||
)
|
||||
|
||||
class FakeWebSocket:
|
||||
def __init__(self) -> None:
|
||||
self.headers = {"authorization": f"Bearer {'x' * 43}"}
|
||||
self.messages = [
|
||||
{"type": "websocket.receive", "bytes": encoded},
|
||||
{"type": "websocket.disconnect"},
|
||||
]
|
||||
self.sent: list[bytes] = []
|
||||
self.accepted = False
|
||||
|
||||
async def accept(self) -> None:
|
||||
self.accepted = True
|
||||
|
||||
async def receive(self) -> dict[str, object]:
|
||||
await asyncio.sleep(0)
|
||||
return self.messages.pop(0)
|
||||
|
||||
async def send_bytes(self, payload: bytes) -> None:
|
||||
self.sent.append(payload)
|
||||
|
||||
async def close(self, **_: object) -> None:
|
||||
return
|
||||
|
||||
websocket = FakeWebSocket()
|
||||
asyncio.run(router.routes[0].endpoint(websocket)) # type: ignore[attr-defined]
|
||||
|
||||
assert websocket.accepted is True
|
||||
assert published.is_set()
|
||||
assert received == [encoded]
|
||||
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from k1link.compute.live_perception import LiveSensorSynchronizer
|
||||
from k1link.data_plane import ConsumerFrameContext, DecodedPointCloudView, DecodedPoseView
|
||||
|
||||
|
||||
def _context(sequence: int, epoch_ns: int) -> ConsumerFrameContext:
|
||||
return ConsumerFrameContext(
|
||||
sequence=sequence,
|
||||
captured_at_epoch_ns=epoch_ns,
|
||||
received_monotonic_ns=epoch_ns,
|
||||
processing_started_monotonic_ns=epoch_ns,
|
||||
encoded_size_bytes=1,
|
||||
live=True,
|
||||
)
|
||||
|
||||
|
||||
def _points(sequence: int, epoch_ns: int) -> DecodedPointCloudView:
|
||||
return DecodedPointCloudView(
|
||||
context=_context(sequence, epoch_ns),
|
||||
frame_id="map",
|
||||
positions_xyz=((1.0, 2.0, 3.0),),
|
||||
intensities=b"\x01",
|
||||
)
|
||||
|
||||
|
||||
def _pose(sequence: int, epoch_ns: int) -> DecodedPoseView:
|
||||
return DecodedPoseView(
|
||||
context=_context(sequence, epoch_ns),
|
||||
frame_id="map",
|
||||
child_frame_id="sensor",
|
||||
position_xyz=(0.0, 0.0, 0.0),
|
||||
orientation_xyzw=(0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
|
||||
|
||||
def test_live_sensor_synchronizer_selects_nearest_bounded_pair() -> None:
|
||||
synchronizer = LiveSensorSynchronizer(
|
||||
maximum_lidar_camera_delta_ms=100,
|
||||
maximum_pose_point_delta_ms=100,
|
||||
)
|
||||
synchronizer.publish_point_cloud(_points(1, 1_000_000_000))
|
||||
synchronizer.publish_point_cloud(_points(2, 1_090_000_000))
|
||||
synchronizer.publish_pose(_pose(1, 1_020_000_000))
|
||||
synchronizer.publish_pose(_pose(2, 1_095_000_000))
|
||||
|
||||
binding = synchronizer.bind_camera(1_080_000_000)
|
||||
|
||||
assert binding.state == "fused-ready"
|
||||
assert binding.point_cloud is not None
|
||||
assert binding.point_cloud.context.sequence == 2
|
||||
assert binding.pose is not None
|
||||
assert binding.pose.context.sequence == 2
|
||||
assert binding.lidar_camera_delta_ms == 10.0
|
||||
assert binding.pose_point_delta_ms == 5.0
|
||||
|
||||
|
||||
def test_live_sensor_synchronizer_reports_each_fail_closed_boundary() -> None:
|
||||
synchronizer = LiveSensorSynchronizer(
|
||||
maximum_lidar_camera_delta_ms=20,
|
||||
maximum_pose_point_delta_ms=10,
|
||||
)
|
||||
assert synchronizer.bind_camera(1_000_000_000).state == "lidar-unavailable"
|
||||
|
||||
synchronizer.publish_point_cloud(_points(1, 900_000_000))
|
||||
assert (
|
||||
synchronizer.bind_camera(1_000_000_000).state
|
||||
== "lidar-camera-delta-exceeded"
|
||||
)
|
||||
|
||||
synchronizer.publish_point_cloud(_points(2, 1_000_000_000))
|
||||
assert synchronizer.bind_camera(1_000_000_000).state == "pose-unavailable"
|
||||
|
||||
synchronizer.publish_pose(_pose(1, 1_100_000_000))
|
||||
assert synchronizer.bind_camera(1_000_000_000).state == "pose-point-delta-exceeded"
|
||||
|
||||
|
||||
def test_live_sensor_synchronizer_waits_only_for_bounded_inflight_pair() -> None:
|
||||
synchronizer = LiveSensorSynchronizer(
|
||||
maximum_lidar_camera_delta_ms=100,
|
||||
maximum_pose_point_delta_ms=100,
|
||||
)
|
||||
|
||||
def publish() -> None:
|
||||
time.sleep(0.02)
|
||||
synchronizer.publish_point_cloud(_points(1, 1_000_000_000))
|
||||
synchronizer.publish_pose(_pose(1, 1_000_000_000))
|
||||
|
||||
thread = threading.Thread(target=publish)
|
||||
thread.start()
|
||||
started = time.monotonic()
|
||||
binding = synchronizer.bind_camera(1_000_000_000, wait_seconds=0.2)
|
||||
elapsed = time.monotonic() - started
|
||||
thread.join()
|
||||
|
||||
assert binding.state == "fused-ready"
|
||||
assert 0.01 <= elapsed < 0.15
|
||||
|
||||
|
||||
def test_live_sensor_synchronizer_storage_never_exceeds_capacity() -> None:
|
||||
synchronizer = LiveSensorSynchronizer(
|
||||
maximum_lidar_camera_delta_ms=100,
|
||||
maximum_pose_point_delta_ms=100,
|
||||
capacity_per_modality=3,
|
||||
retention_seconds=100,
|
||||
)
|
||||
for sequence in range(1, 8):
|
||||
synchronizer.publish_point_cloud(_points(sequence, sequence * 1_000_000))
|
||||
synchronizer.publish_pose(_pose(sequence, sequence * 1_000_000))
|
||||
|
||||
snapshot = synchronizer.snapshot()
|
||||
assert snapshot["point_depth"] == 3
|
||||
assert snapshot["pose_depth"] == 3
|
||||
assert snapshot["evicted_points"] == 4
|
||||
assert snapshot["evicted_poses"] == 4
|
||||
@@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.live_perception import TELEMETRY_SCHEMA, WORLD_STATE_SCHEMA
|
||||
from k1link.compute.live_replay_qualification import (
|
||||
_validate_telemetry_rows,
|
||||
_validate_world_rows,
|
||||
)
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
|
||||
def _world_row() -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": WORLD_STATE_SCHEMA,
|
||||
"frame_index": 3,
|
||||
"source_frame_index": 1003,
|
||||
"session_seconds": 12.3,
|
||||
"coordinate_frames": {
|
||||
"world": "k1-map",
|
||||
"sensor_relative": "k1-lidar",
|
||||
"vehicle_body": "unavailable-no-rig-to-vehicle-transform",
|
||||
},
|
||||
"fusion_state": "fused",
|
||||
"objects": [
|
||||
{
|
||||
"track_id": 7,
|
||||
"range_m": 4.0,
|
||||
"position_map_m": [1.0, 2.0, 3.0],
|
||||
"position_lidar_m": [4.0, 0.0, 0.0],
|
||||
"size_m": [4.0, 2.0, 1.5],
|
||||
"velocity_map_mps": None,
|
||||
"velocity_status": "unavailable-insufficient-history",
|
||||
"geometry": "point-supported-visible-surface-envelope",
|
||||
}
|
||||
],
|
||||
"object_count": 1,
|
||||
"clearance": {"state": "observed"},
|
||||
"delivery": {"health": "healthy", "result_age_ms": 12.0},
|
||||
}
|
||||
|
||||
|
||||
def test_world_and_telemetry_rows_preserve_exact_frame_binding(tmp_path: Path) -> None:
|
||||
world = tmp_path / "world-state.jsonl"
|
||||
telemetry = tmp_path / "telemetry.jsonl"
|
||||
world.write_text(json.dumps(_world_row()) + "\n", encoding="utf-8")
|
||||
telemetry.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": TELEMETRY_SCHEMA,
|
||||
"frame_index": 3,
|
||||
"end_to_end_latency_ms": 12.0,
|
||||
"health": "healthy",
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
indices = _validate_world_rows(
|
||||
world,
|
||||
tmp_path,
|
||||
expected_count=1,
|
||||
selection_start=3,
|
||||
selection_count=1,
|
||||
)
|
||||
_validate_telemetry_rows(telemetry, tmp_path, expected_indices=indices)
|
||||
|
||||
assert indices == (3,)
|
||||
|
||||
|
||||
def test_world_row_rejects_invented_object_count(tmp_path: Path) -> None:
|
||||
world = tmp_path / "world-state.jsonl"
|
||||
value = _world_row()
|
||||
value["object_count"] = 2
|
||||
world.write_text(json.dumps(value) + "\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="world-state row"):
|
||||
_validate_world_rows(
|
||||
world,
|
||||
tmp_path,
|
||||
expected_count=1,
|
||||
selection_start=3,
|
||||
selection_count=1,
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import plistlib
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).parents[1]
|
||||
MODULE_PATH = (
|
||||
REPOSITORY_ROOT
|
||||
/ "plugins"
|
||||
/ "xgrids-k1"
|
||||
/ "lab"
|
||||
/ "iphone-capture"
|
||||
/ "targeted_profile_backup.py"
|
||||
)
|
||||
|
||||
|
||||
def _load_module() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location("targeted_profile_backup", MODULE_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
TARGETED = _load_module()
|
||||
|
||||
|
||||
def _synthetic_backup(tmp_path: Path, payload: bytes) -> Path:
|
||||
device = tmp_path / "device"
|
||||
device.mkdir()
|
||||
(device / "Manifest.plist").write_bytes(
|
||||
plistlib.dumps({"IsEncrypted": False}, fmt=plistlib.FMT_BINARY)
|
||||
)
|
||||
file_id = TARGETED.PREFERENCES_FILE_ID
|
||||
source = device / file_id[:2] / file_id
|
||||
source.parent.mkdir()
|
||||
source.write_bytes(payload)
|
||||
with sqlite3.connect(device / "Manifest.db") as connection:
|
||||
connection.execute(
|
||||
"CREATE TABLE Files (fileID TEXT, domain TEXT, relativePath TEXT)"
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT INTO Files VALUES (?, ?, ?)",
|
||||
(file_id, TARGETED.APP_DOMAIN, TARGETED.PREFERENCES_PATH),
|
||||
)
|
||||
return device
|
||||
|
||||
|
||||
def test_profile_filter_is_exact_to_lixelgo_preferences() -> None:
|
||||
assert TARGETED._is_preferences_backup_file(
|
||||
SimpleNamespace(
|
||||
file_name=None,
|
||||
device_name=None,
|
||||
domain=TARGETED.APP_DOMAIN,
|
||||
relative_path=TARGETED.PREFERENCES_PATH,
|
||||
)
|
||||
)
|
||||
assert not TARGETED._is_preferences_backup_file(
|
||||
SimpleNamespace(
|
||||
file_name=None,
|
||||
device_name=None,
|
||||
domain=TARGETED.APP_DOMAIN,
|
||||
relative_path="Documents/large-project.las",
|
||||
)
|
||||
)
|
||||
assert not TARGETED._is_preferences_backup_file(
|
||||
SimpleNamespace(
|
||||
file_name=None,
|
||||
device_name=None,
|
||||
domain="AppDomain-com.example.Other",
|
||||
relative_path=TARGETED.PREFERENCES_PATH,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_profile_file_id_filter_matches_only_the_exact_backup_payload() -> None:
|
||||
file_id = TARGETED.PREFERENCES_FILE_ID
|
||||
assert TARGETED._is_preferences_backup_file(
|
||||
SimpleNamespace(file_name=f"device/{file_id[:2]}/{file_id}")
|
||||
)
|
||||
assert TARGETED._is_preferences_backup_file(
|
||||
SimpleNamespace(file_name=None, device_name=f"device/{file_id[:2]}/{file_id}")
|
||||
)
|
||||
assert not TARGETED._is_preferences_backup_file(
|
||||
SimpleNamespace(file_name=f"device/{file_id[:2]}/{file_id}0")
|
||||
)
|
||||
assert not TARGETED._is_preferences_backup_file(
|
||||
SimpleNamespace(file_name="device/aa/" + "a" * 40)
|
||||
)
|
||||
|
||||
|
||||
def test_read_preferences_accepts_one_small_unencrypted_plist(tmp_path: Path) -> None:
|
||||
payload = plistlib.dumps({"opaque-key": "opaque-value"}, fmt=plistlib.FMT_BINARY)
|
||||
device = _synthetic_backup(tmp_path, payload)
|
||||
|
||||
assert TARGETED._read_preferences(device) == payload
|
||||
|
||||
|
||||
def test_read_preferences_rejects_encrypted_backup(tmp_path: Path) -> None:
|
||||
payload = plistlib.dumps({"opaque-key": "opaque-value"}, fmt=plistlib.FMT_BINARY)
|
||||
device = _synthetic_backup(tmp_path, payload)
|
||||
(device / "Manifest.plist").write_bytes(
|
||||
plistlib.dumps({"IsEncrypted": True}, fmt=plistlib.FMT_BINARY)
|
||||
)
|
||||
|
||||
with pytest.raises(TARGETED.TargetedProfileBackupError, match="encrypted"):
|
||||
TARGETED._read_preferences(device)
|
||||
@@ -183,6 +183,27 @@ def test_capture_writes_verifiable_frames_metadata_and_summary(tmp_path: Path) -
|
||||
assert stat.S_IMODE((capture_dir / artifact_name).stat().st_mode) == 0o600
|
||||
|
||||
|
||||
def test_capture_without_duration_runs_until_explicit_stop(tmp_path: Path) -> None:
|
||||
fake = FakeClient()
|
||||
stop_checks = 0
|
||||
|
||||
def should_stop() -> bool:
|
||||
nonlocal stop_checks
|
||||
stop_checks += 1
|
||||
return stop_checks >= 4
|
||||
|
||||
summary = capture_mqtt(
|
||||
"192.168.1.50",
|
||||
tmp_path / "unbounded-capture",
|
||||
duration_seconds=None,
|
||||
should_stop=should_stop,
|
||||
_client_factory=lambda: cast(mqtt.Client, fake),
|
||||
)
|
||||
|
||||
assert summary["requested_duration_seconds"] is None
|
||||
assert summary["stop_reason"] == "external_stop"
|
||||
|
||||
|
||||
def test_oversize_payload_is_not_written_to_raw_capture(tmp_path: Path) -> None:
|
||||
fake = FakeClient(payload=b"oversize")
|
||||
capture_dir = tmp_path / "capture"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.multirate_perception_qualification import (
|
||||
MERGED_FRAME_SCHEMA,
|
||||
_validate_merged_rows,
|
||||
)
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
|
||||
def _merged(index: int, semantic: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": MERGED_FRAME_SCHEMA,
|
||||
"frame_index": index,
|
||||
"source_frame_index": 1000 + index,
|
||||
"session_seconds": 20.0 + index * 0.1,
|
||||
"detector_result_age_ms": 80.0,
|
||||
"tracks": [],
|
||||
"semantic": semantic,
|
||||
}
|
||||
|
||||
|
||||
def test_merged_rows_allow_explicit_unavailable_then_fresh(tmp_path: Path) -> None:
|
||||
path = tmp_path / "merged-frames.jsonl"
|
||||
unavailable = {
|
||||
"status": "unavailable",
|
||||
"source_frame_index": None,
|
||||
"source_age_ms": None,
|
||||
"completion_age_ms": None,
|
||||
"mask_sha256": None,
|
||||
}
|
||||
fresh = {
|
||||
"status": "fresh",
|
||||
"source_frame_index": 1000,
|
||||
"source_age_ms": 200.0,
|
||||
"completion_age_ms": 210.0,
|
||||
"mask_sha256": "a" * 64,
|
||||
}
|
||||
path.write_text(
|
||||
json.dumps(_merged(0, unavailable)) + "\n" + json.dumps(_merged(2, fresh)) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
_validate_merged_rows(
|
||||
path,
|
||||
tmp_path,
|
||||
expected_indices=(0, 2),
|
||||
source_start=1000,
|
||||
semantic_indices={0},
|
||||
)
|
||||
|
||||
|
||||
def test_merged_rows_reject_future_semantic_binding(tmp_path: Path) -> None:
|
||||
path = tmp_path / "merged-frames.jsonl"
|
||||
future = {
|
||||
"status": "fresh",
|
||||
"source_frame_index": 1005,
|
||||
"source_age_ms": 0.0,
|
||||
"completion_age_ms": 200.0,
|
||||
"mask_sha256": "a" * 64,
|
||||
}
|
||||
path.write_text(json.dumps(_merged(2, future)) + "\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="binding"):
|
||||
_validate_merged_rows(
|
||||
path,
|
||||
tmp_path,
|
||||
expected_indices=(2,),
|
||||
source_start=1000,
|
||||
semantic_indices={5},
|
||||
)
|
||||
@@ -0,0 +1,380 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from k1link.compute import (
|
||||
EvaluationFrameRequest,
|
||||
RecordedEvaluationPackError,
|
||||
RecordedQualificationSliceError,
|
||||
prepare_camera_compute_job,
|
||||
prepare_recorded_evaluation_pack,
|
||||
prepare_recorded_qualification_slice,
|
||||
validate_recorded_evaluation_pack,
|
||||
validate_recorded_qualification_slice,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.analyze import (
|
||||
K1ValidFovMaskError,
|
||||
prepare_k1_valid_fov_mask,
|
||||
validate_k1_valid_fov_mask,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.calibration_schema import (
|
||||
parse_k1_factory_calibration,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.calibration_snapshot import (
|
||||
CALIBRATION_SNAPSHOT_MANIFEST_VERSION,
|
||||
)
|
||||
from k1link.web.camera_archive import CameraArchiveWriter
|
||||
|
||||
CALIBRATION_FIXTURES = Path(__file__).parent / "fixtures" / "k1" / "calibration"
|
||||
|
||||
|
||||
def _box(box_type: bytes, payload: bytes = b"") -> bytes:
|
||||
return (8 + len(payload)).to_bytes(4, "big") + box_type + payload
|
||||
|
||||
|
||||
def _full_box(box_type: bytes, payload: bytes = b"", *, flags: int = 0) -> bytes:
|
||||
return _box(box_type, bytes([0]) + flags.to_bytes(3, "big") + payload)
|
||||
|
||||
|
||||
def _recorded_h264_fixture(base_decode_time: int = 0) -> tuple[bytes, bytes]:
|
||||
track_id = 1
|
||||
sample_duration = 500
|
||||
tkhd = _full_box(
|
||||
b"tkhd",
|
||||
b"\x00" * 8 + track_id.to_bytes(4, "big") + b"\x00" * 4,
|
||||
)
|
||||
mdhd = _full_box(
|
||||
b"mdhd",
|
||||
b"\x00" * 8 + (1_000).to_bytes(4, "big") + b"\x00" * 4,
|
||||
)
|
||||
hdlr = _full_box(b"hdlr", b"\x00" * 4 + b"vide")
|
||||
trak = _box(b"trak", tkhd + _box(b"mdia", mdhd + hdlr))
|
||||
trex = _full_box(
|
||||
b"trex",
|
||||
track_id.to_bytes(4, "big")
|
||||
+ (1).to_bytes(4, "big")
|
||||
+ sample_duration.to_bytes(4, "big")
|
||||
+ b"\x00" * 8,
|
||||
)
|
||||
init = _box(b"ftyp", b"isom") + _box(
|
||||
b"moov",
|
||||
trak + _box(b"mvex", trex) + _box(b"avcC", b"\x01\x64\x00\x28"),
|
||||
)
|
||||
tfhd = _full_box(b"tfhd", track_id.to_bytes(4, "big"), flags=0x020000)
|
||||
tfdt = _full_box(b"tfdt", base_decode_time.to_bytes(4, "big"))
|
||||
trun = _full_box(b"trun", (1).to_bytes(4, "big"))
|
||||
fragment = _box(b"moof", _box(b"traf", tfhd + tfdt + trun)) + _box(
|
||||
b"mdat",
|
||||
b"frame",
|
||||
)
|
||||
return init, fragment
|
||||
|
||||
|
||||
def _camera_job(tmp_path: Path, frame_count: int = 9) -> Path:
|
||||
session = tmp_path / "session-qualification"
|
||||
session.mkdir()
|
||||
init, _fragment = _recorded_h264_fixture()
|
||||
writer = CameraArchiveWriter(session, "sensor.camera.right", 1)
|
||||
writer.append(
|
||||
"init",
|
||||
init,
|
||||
host_epoch_ns=1_000_000_000,
|
||||
host_monotonic_ns=2_000_000_000,
|
||||
)
|
||||
for index in range(frame_count):
|
||||
_init, fragment = _recorded_h264_fixture(index * 500)
|
||||
writer.append(
|
||||
"media",
|
||||
fragment,
|
||||
host_epoch_ns=1_500_000_000 + index * 500_000_000,
|
||||
host_monotonic_ns=2_500_000_000 + index * 500_000_000,
|
||||
)
|
||||
writer.close()
|
||||
return prepare_camera_compute_job(
|
||||
session_root=session,
|
||||
source_id="sensor.camera.right",
|
||||
codec_epoch=1,
|
||||
origin_epoch_ns=1_000_000_000,
|
||||
origin_monotonic_ns=2_000_000_000,
|
||||
output_root=tmp_path / "jobs",
|
||||
).job_root
|
||||
|
||||
|
||||
def _calibration_snapshot(tmp_path: Path) -> Path:
|
||||
root = tmp_path / "calibration"
|
||||
root.mkdir()
|
||||
camera = (CALIBRATION_FIXTURES / "camera.yaml").read_bytes()
|
||||
extrinsic = (CALIBRATION_FIXTURES / "extrinsic_camera_lidar.yaml").read_bytes()
|
||||
(root / "camera.yaml").write_bytes(camera)
|
||||
(root / "extrinsic_camera_lidar.yaml").write_bytes(extrinsic)
|
||||
vendor_device_id = "fixture-device"
|
||||
device_serial = "fixture-serial"
|
||||
artifact_values = (
|
||||
(
|
||||
"/mnt/system/factory-data/config/camera.yaml",
|
||||
"camera.yaml",
|
||||
camera,
|
||||
),
|
||||
(
|
||||
"/mnt/system/factory-data/config/extrinsic_camera_lidar.yaml",
|
||||
"extrinsic_camera_lidar.yaml",
|
||||
extrinsic,
|
||||
),
|
||||
)
|
||||
artifacts = [
|
||||
{
|
||||
"source_path": source_path,
|
||||
"artifact_name": name,
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"bytes": len(payload),
|
||||
"encoding": "utf-8",
|
||||
}
|
||||
for source_path, name, payload in artifact_values
|
||||
]
|
||||
content_identity = hashlib.sha256()
|
||||
content_identity.update(vendor_device_id.encode("ascii"))
|
||||
content_identity.update(b"\x00")
|
||||
content_identity.update(device_serial.encode("ascii"))
|
||||
for artifact in sorted(artifacts, key=lambda item: str(item["source_path"])):
|
||||
content_identity.update(b"\x00")
|
||||
content_identity.update(str(artifact["source_path"]).encode("utf-8"))
|
||||
content_identity.update(bytes.fromhex(str(artifact["sha256"])))
|
||||
calibration = parse_k1_factory_calibration(camera, extrinsic)
|
||||
manifest = {
|
||||
"schema_version": CALIBRATION_SNAPSHOT_MANIFEST_VERSION,
|
||||
"captured_at_utc": datetime.now(UTC).isoformat(),
|
||||
"content_identity_sha256": content_identity.hexdigest(),
|
||||
"device": {
|
||||
"vendor_device_id": vendor_device_id,
|
||||
"device_serial": device_serial,
|
||||
},
|
||||
"artifacts": artifacts,
|
||||
"normalized_calibration": calibration.normalized_profile(),
|
||||
}
|
||||
(root / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
return root
|
||||
|
||||
|
||||
def test_valid_fov_mask_is_calibration_bound_binary_and_reusable(tmp_path: Path) -> None:
|
||||
snapshot = _calibration_snapshot(tmp_path)
|
||||
first = prepare_k1_valid_fov_mask(
|
||||
calibration_snapshot_root=snapshot,
|
||||
source_id="sensor.camera.right",
|
||||
output_root=tmp_path / "masks",
|
||||
)
|
||||
repeated = prepare_k1_valid_fov_mask(
|
||||
calibration_snapshot_root=snapshot,
|
||||
source_id="sensor.camera.right",
|
||||
output_root=tmp_path / "masks",
|
||||
)
|
||||
|
||||
assert repeated == first
|
||||
assert first.calibration_slot == "camera_1"
|
||||
assert (first.width, first.height) == (800, 600)
|
||||
calibration = parse_k1_factory_calibration(
|
||||
(CALIBRATION_FIXTURES / "camera.yaml").read_bytes(),
|
||||
(CALIBRATION_FIXTURES / "extrinsic_camera_lidar.yaml").read_bytes(),
|
||||
)
|
||||
camera = calibration.camera("camera_1")
|
||||
expected_center = (camera.intrinsic[2] * 0.2, camera.intrinsic[3] * 0.2)
|
||||
expected_radius = min(
|
||||
expected_center[0],
|
||||
799 - expected_center[0],
|
||||
expected_center[1],
|
||||
599 - expected_center[1],
|
||||
) - 4.0
|
||||
assert first.center_xy == pytest.approx(expected_center)
|
||||
assert first.radius_pixels == pytest.approx(expected_radius)
|
||||
assert 0.55 < first.valid_fraction < 0.57
|
||||
mask = np.asarray(Image.open(first.mask_path), dtype=np.uint8)
|
||||
assert set(np.unique(mask)) == {0, 255}
|
||||
assert mask[0, 0] == 0
|
||||
assert mask[round(first.center_xy[1]), round(first.center_xy[0])] == 255
|
||||
assert validate_k1_valid_fov_mask(first.root) == first
|
||||
|
||||
|
||||
def test_valid_fov_mask_rejects_changed_png(tmp_path: Path) -> None:
|
||||
result = prepare_k1_valid_fov_mask(
|
||||
calibration_snapshot_root=_calibration_snapshot(tmp_path),
|
||||
source_id="sensor.camera.right",
|
||||
output_root=tmp_path / "masks",
|
||||
)
|
||||
result.mask_path.write_bytes(b"changed")
|
||||
|
||||
with pytest.raises(K1ValidFovMaskError, match="artifact changed"):
|
||||
validate_k1_valid_fov_mask(result.root)
|
||||
|
||||
|
||||
def test_qualification_slice_is_uniform_job_bound_and_reusable(tmp_path: Path) -> None:
|
||||
job_root = _camera_job(tmp_path)
|
||||
first = prepare_recorded_qualification_slice(
|
||||
job_root=job_root,
|
||||
output_root=tmp_path / "slices",
|
||||
sample_count=4,
|
||||
)
|
||||
repeated = prepare_recorded_qualification_slice(
|
||||
job_root=job_root,
|
||||
output_root=tmp_path / "slices",
|
||||
sample_count=4,
|
||||
)
|
||||
|
||||
assert repeated == first
|
||||
assert first.source_frame_count == 9
|
||||
assert tuple(frame.frame_index for frame in first.frames) == (0, 3, 5, 8)
|
||||
assert tuple(frame.sequence for frame in first.frames) == (1, 4, 6, 9)
|
||||
assert validate_recorded_qualification_slice(first.root, job_root=job_root) == first
|
||||
|
||||
|
||||
def test_qualification_slice_rejects_changed_selection(tmp_path: Path) -> None:
|
||||
job_root = _camera_job(tmp_path)
|
||||
result = prepare_recorded_qualification_slice(
|
||||
job_root=job_root,
|
||||
output_root=tmp_path / "slices",
|
||||
sample_count=4,
|
||||
)
|
||||
manifest = json.loads(result.manifest_path.read_text(encoding="utf-8"))
|
||||
manifest["frames"][1]["frame_index"] = 2
|
||||
result.manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
|
||||
with pytest.raises(RecordedQualificationSliceError, match="binding changed"):
|
||||
validate_recorded_qualification_slice(result.root, job_root=job_root)
|
||||
|
||||
|
||||
def _evaluation_fixture(tmp_path: Path) -> tuple[Path, Path, Path, Path, Path, tuple[Any, ...]]:
|
||||
job_root = _camera_job(tmp_path, frame_count=64)
|
||||
qualification = prepare_recorded_qualification_slice(
|
||||
job_root=job_root,
|
||||
output_root=tmp_path / "slices",
|
||||
sample_count=16,
|
||||
)
|
||||
valid_fov = prepare_k1_valid_fov_mask(
|
||||
calibration_snapshot_root=_calibration_snapshot(tmp_path),
|
||||
source_id="sensor.camera.right",
|
||||
output_root=tmp_path / "masks",
|
||||
)
|
||||
anchors = {frame.frame_index for frame in qualification.frames}
|
||||
temporal = {
|
||||
1: "clip-a",
|
||||
2: "clip-a",
|
||||
3: "clip-a",
|
||||
60: "clip-b",
|
||||
61: "clip-b",
|
||||
62: "clip-b",
|
||||
}
|
||||
requests = tuple(
|
||||
EvaluationFrameRequest(
|
||||
frame_index=index,
|
||||
role="anchor" if index in anchors else "temporal",
|
||||
group_id=f"anchor-{index:06d}" if index in anchors else temporal[index],
|
||||
)
|
||||
for index in sorted(anchors | set(temporal))
|
||||
)
|
||||
frames_root = tmp_path / "decoded"
|
||||
frames_root.mkdir()
|
||||
for request in requests:
|
||||
image = Image.new(
|
||||
"RGB",
|
||||
(800, 600),
|
||||
(request.frame_index, request.frame_index // 2, 255 - request.frame_index),
|
||||
)
|
||||
image.save(frames_root / f"frame-{request.frame_index:06d}.png")
|
||||
timeline_path = tmp_path / "timeline.jsonl"
|
||||
timeline_path.write_text(
|
||||
"".join(
|
||||
json.dumps({"frame_index": index, "session_seconds": 0.5 + index * 0.5}) + "\n"
|
||||
for index in range(64)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return (
|
||||
job_root,
|
||||
qualification.root,
|
||||
valid_fov.root,
|
||||
frames_root,
|
||||
timeline_path,
|
||||
requests,
|
||||
)
|
||||
|
||||
|
||||
def test_evaluation_pack_is_bound_reusable_and_separates_annotation_state(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
job_root, qualification_root, valid_fov_root, frames_root, timeline_path, requests = (
|
||||
_evaluation_fixture(tmp_path)
|
||||
)
|
||||
first = prepare_recorded_evaluation_pack(
|
||||
job_root=job_root,
|
||||
qualification_root=qualification_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
decoded_frames_root=frames_root,
|
||||
timeline_path=timeline_path,
|
||||
output_root=tmp_path / "evaluation-packs",
|
||||
selection=requests,
|
||||
decoder_version="fixture-decoder/v1",
|
||||
selection_document_sha256="1" * 64,
|
||||
producer_files=(("fixture.py", "2" * 64),),
|
||||
)
|
||||
repeated = prepare_recorded_evaluation_pack(
|
||||
job_root=job_root,
|
||||
qualification_root=qualification_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
decoded_frames_root=frames_root,
|
||||
timeline_path=timeline_path,
|
||||
output_root=tmp_path / "evaluation-packs",
|
||||
selection=requests,
|
||||
decoder_version="fixture-decoder/v1",
|
||||
selection_document_sha256="1" * 64,
|
||||
producer_files=(("fixture.py", "2" * 64),),
|
||||
)
|
||||
|
||||
assert repeated == first
|
||||
assert len(first.frames) == 22
|
||||
assert sum(frame.role == "anchor" for frame in first.frames) == 16
|
||||
assert sum(frame.role == "temporal" for frame in first.frames) == 6
|
||||
template = json.loads(first.annotation_template_path.read_text(encoding="utf-8"))
|
||||
assert template["state"] == "unannotated"
|
||||
assert {row["annotation_status"] for row in template["reviews"]} == {"unannotated"}
|
||||
assert (
|
||||
validate_recorded_evaluation_pack(
|
||||
first.root,
|
||||
job_root=job_root,
|
||||
qualification_root=qualification_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
)
|
||||
== first
|
||||
)
|
||||
|
||||
|
||||
def test_evaluation_pack_rejects_changed_image(tmp_path: Path) -> None:
|
||||
job_root, qualification_root, valid_fov_root, frames_root, timeline_path, requests = (
|
||||
_evaluation_fixture(tmp_path)
|
||||
)
|
||||
result = prepare_recorded_evaluation_pack(
|
||||
job_root=job_root,
|
||||
qualification_root=qualification_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
decoded_frames_root=frames_root,
|
||||
timeline_path=timeline_path,
|
||||
output_root=tmp_path / "evaluation-packs",
|
||||
selection=requests,
|
||||
decoder_version="fixture-decoder/v1",
|
||||
selection_document_sha256="1" * 64,
|
||||
producer_files=(("fixture.py", "2" * 64),),
|
||||
)
|
||||
result.frames[0].valid_fov_fill_path.write_bytes(b"changed")
|
||||
|
||||
with pytest.raises(RecordedEvaluationPackError, match="artifact changed"):
|
||||
validate_recorded_evaluation_pack(
|
||||
result.root,
|
||||
job_root=job_root,
|
||||
qualification_root=qualification_root,
|
||||
valid_fov_root=valid_fov_root,
|
||||
)
|
||||
@@ -20,6 +20,7 @@ from pydantic import ValidationError
|
||||
|
||||
import k1link.web.device_plugin_composition as plugin_composition
|
||||
from k1link.device_plugins.xgrids_k1.facade import (
|
||||
ACTION_DEVICE_CALIBRATION_SNAPSHOT_READ,
|
||||
ACTION_DISCOVERY_SCAN,
|
||||
ACTION_NETWORK_PROVISION,
|
||||
ACTION_STREAM_START_LIVE,
|
||||
@@ -59,6 +60,10 @@ class FakeXgridsService:
|
||||
self.calls.append(("state", None))
|
||||
return {"phase": "idle"}
|
||||
|
||||
def read_device_calibration_snapshot(self) -> dict[str, Any]:
|
||||
self.calls.append(("calibration", None))
|
||||
return {"status": "available", "snapshot_id": "fixture-snapshot"}
|
||||
|
||||
async def scan_ble(self, duration_seconds: float) -> dict[str, Any]:
|
||||
self.calls.append(("scan", duration_seconds))
|
||||
return {"phase": "idle", "devices": []}
|
||||
@@ -71,7 +76,7 @@ class FakeXgridsService:
|
||||
self,
|
||||
project_name: str,
|
||||
host: str | None,
|
||||
duration_seconds: float,
|
||||
duration_seconds: float | None,
|
||||
compatibility_attestation: CompatibilityAttestationRequest,
|
||||
) -> dict[str, Any]:
|
||||
self.calls.append(
|
||||
@@ -135,6 +140,24 @@ def test_manifest_and_runtime_facade_declare_identical_actions() -> None:
|
||||
assert {action.id for action in manifest.spec.actions} == XgridsK1PluginFacade.action_ids
|
||||
|
||||
|
||||
def test_calibration_snapshot_action_calls_the_read_only_service_method() -> None:
|
||||
service = FakeXgridsService()
|
||||
dispatcher = DevicePluginDispatcher(
|
||||
[_in_process_runtime(XgridsK1PluginFacade(service))]
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
dispatcher.invoke(
|
||||
XGRIDS_K1_PLUGIN_ID,
|
||||
ACTION_DEVICE_CALIBRATION_SNAPSHOT_READ,
|
||||
{},
|
||||
)
|
||||
)
|
||||
|
||||
assert result == {"status": "available", "snapshot_id": "fixture-snapshot"}
|
||||
assert service.calls == [("calibration", None)]
|
||||
|
||||
|
||||
def test_repository_runtime_composition_exactly_matches_catalog() -> None:
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
environment = load_installed_device_plugins(repository_root)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.realtime_tracking_qualification import (
|
||||
FRAME_SCHEMA,
|
||||
TELEMETRY_SCHEMA,
|
||||
_validate_realtime_frames,
|
||||
_validate_realtime_telemetry,
|
||||
)
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
|
||||
def _frame(index: int) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": FRAME_SCHEMA,
|
||||
"frame_index": index,
|
||||
"sequence": index + 1,
|
||||
"source_frame_index": 1000 + index,
|
||||
"source_sequence": 1001 + index,
|
||||
"session_seconds": 135.0 + index * 0.1,
|
||||
"detections": [],
|
||||
"tracks": [],
|
||||
"delivery": {"health": "healthy", "result_age_ms": 50.0},
|
||||
}
|
||||
|
||||
|
||||
def _telemetry(index: int) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": TELEMETRY_SCHEMA,
|
||||
"frame_index": index,
|
||||
"session_seconds": 135.0 + index * 0.1,
|
||||
"health": "healthy",
|
||||
"result_age_ms": 50.0,
|
||||
"processing_ms": 20.0,
|
||||
"queue_depth_after_take": 0,
|
||||
"queue_dropped_overflow": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_realtime_rows_allow_explicit_latest_wins_gaps(tmp_path: Path) -> None:
|
||||
indices = (0, 2, 3)
|
||||
frames = tmp_path / "frames.jsonl"
|
||||
telemetry = tmp_path / "telemetry.jsonl"
|
||||
frames.write_text(
|
||||
"".join(json.dumps(_frame(index)) + "\n" for index in indices),
|
||||
encoding="utf-8",
|
||||
)
|
||||
telemetry.write_text(
|
||||
"".join(json.dumps(_telemetry(index)) + "\n" for index in indices),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
observed = _validate_realtime_frames(
|
||||
frames,
|
||||
tmp_path,
|
||||
expected_count=3,
|
||||
selection_count=4,
|
||||
source_start=1000,
|
||||
timeline_start=135.0,
|
||||
timeline_end=135.3,
|
||||
)
|
||||
_validate_realtime_telemetry(telemetry, tmp_path, expected_indices=observed)
|
||||
|
||||
assert observed == indices
|
||||
|
||||
|
||||
def test_realtime_rows_reject_out_of_order_results(tmp_path: Path) -> None:
|
||||
frames = tmp_path / "frames.jsonl"
|
||||
frames.write_text(
|
||||
json.dumps(_frame(1)) + "\n" + json.dumps(_frame(0)) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="frame metadata"):
|
||||
_validate_realtime_frames(
|
||||
frames,
|
||||
tmp_path,
|
||||
expected_count=2,
|
||||
selection_count=4,
|
||||
source_start=1000,
|
||||
timeline_start=135.0,
|
||||
timeline_end=135.3,
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _worker_module() -> ModuleType:
|
||||
path = (
|
||||
Path(__file__).parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "run_recorded_perception_epoch.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("recorded_perception_worker", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _job(root: Path) -> dict[str, object]:
|
||||
payload = root / "input" / "payload.bin"
|
||||
payload.parent.mkdir(parents=True)
|
||||
payload.write_bytes(b"immutable-camera-input")
|
||||
input_document = {
|
||||
"byte_length": payload.stat().st_size,
|
||||
"files": [
|
||||
{
|
||||
"path": "input/payload.bin",
|
||||
"byte_length": payload.stat().st_size,
|
||||
"sha256": hashlib.sha256(payload.read_bytes()).hexdigest(),
|
||||
}
|
||||
],
|
||||
}
|
||||
encoded = json.dumps(
|
||||
input_document,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
input_sha256 = hashlib.sha256(encoded).hexdigest()
|
||||
document: dict[str, object] = {
|
||||
"schema_version": "missioncore.compute-job/v1",
|
||||
"job_id": f"recorded-camera-{input_sha256[:24]}",
|
||||
"input_sha256": input_sha256,
|
||||
"input": input_document,
|
||||
}
|
||||
(root / "job.json").write_text(json.dumps(document), encoding="utf-8")
|
||||
return document
|
||||
|
||||
|
||||
def test_worker_validates_job_identity_independently_of_container_mount_name(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
worker = _worker_module()
|
||||
mounted_root = tmp_path / "job"
|
||||
mounted_root.mkdir()
|
||||
expected = _job(mounted_root)
|
||||
|
||||
validated = worker._validate_job(mounted_root)
|
||||
|
||||
assert validated["job_id"] == expected["job_id"]
|
||||
assert mounted_root.name != expected["job_id"]
|
||||
|
||||
|
||||
def test_worker_rejects_a_transferred_payload_that_changed(tmp_path: Path) -> None:
|
||||
worker = _worker_module()
|
||||
mounted_root = tmp_path / "job"
|
||||
mounted_root.mkdir()
|
||||
_job(mounted_root)
|
||||
(mounted_root / "input" / "payload.bin").write_bytes(b"changed")
|
||||
|
||||
with pytest.raises(RuntimeError, match="compute job artifact changed"):
|
||||
worker._validate_job(mounted_root)
|
||||
@@ -9,6 +9,7 @@ from pathlib import Path
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.compute.live_perception import LivePerceptionResultFrame
|
||||
from k1link.data_plane import DecodedDataPlaneView, NormalizationError
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
|
||||
from k1link.device_plugins.xgrids_k1.protocol.normalizer import normalize_k1_message
|
||||
@@ -138,6 +139,43 @@ def test_live_blueprint_follows_stream_time_without_frontend_cursor_writes() ->
|
||||
assert panel.state == "hidden"
|
||||
|
||||
|
||||
def test_live_perception_logs_original_mask_2d_distance_and_3d_cuboid() -> None:
|
||||
recording = FakeRecording()
|
||||
bridge = RerunBridge(recording_factory=lambda _: recording) # type: ignore[arg-type]
|
||||
mask = np.zeros((600, 800), dtype=np.uint8)
|
||||
mask[20:30, 40:50] = 4
|
||||
|
||||
bridge.process_perception(
|
||||
LivePerceptionResultFrame(
|
||||
frame_index=3,
|
||||
source_frame_index=30,
|
||||
session_seconds=1.0,
|
||||
captured_at_epoch_ns=1_784_124_315_186_225_000,
|
||||
image_jpeg=b"\xff\xd8test\xff\xd9",
|
||||
segmentation_mask=mask,
|
||||
objects=(
|
||||
{
|
||||
"track_id": 9,
|
||||
"label": "car",
|
||||
"score": 0.9,
|
||||
"bbox_xyxy": [10.0, 20.0, 110.0, 80.0],
|
||||
"distance_m": 6.2,
|
||||
"cuboid_center_map": [1.0, 2.0, 0.5],
|
||||
"cuboid_half_size": [2.25, 0.925, 0.775],
|
||||
"cuboid_quaternion_xyzw": [0.0, 0.0, 0.0, 1.0],
|
||||
},
|
||||
),
|
||||
delivery={"health": "healthy"},
|
||||
)
|
||||
)
|
||||
|
||||
paths = [path for path, _, _ in recording.logs]
|
||||
assert "/perception/camera/image" in paths
|
||||
assert "/perception/camera/segmentation" in paths
|
||||
assert "/perception/camera/detections" in paths
|
||||
assert "/world/perception/boxes3d" in paths
|
||||
|
||||
|
||||
def test_constructor_disconnects_recording_after_partial_setup_failure() -> None:
|
||||
recording = BlueprintFailureRecording()
|
||||
|
||||
|
||||
+174
-2
@@ -19,16 +19,23 @@ from k1link.device_plugins.xgrids_k1.mqtt.capture import (
|
||||
from k1link.device_plugins.xgrids_k1.rrd_export import (
|
||||
RECORDED_CAMERA_VIEW_ID,
|
||||
RECORDED_METRICS_VIEW_ID,
|
||||
RECORDED_PERCEPTION_3D_VIEW_ID,
|
||||
RECORDED_POINTS_VISUALIZER_ID,
|
||||
RECORDED_ROOT_CONTAINER_ID,
|
||||
RECORDED_SPATIAL_VIEW_ID,
|
||||
RECORDED_VIEW_POINT_DECIMATION_THRESHOLD,
|
||||
RECORDED_VIEW_POINT_FRAME_STRIDE,
|
||||
RECORDED_VIEW_POINT_STRIDE,
|
||||
SESSION_TIMELINE,
|
||||
RrdExportCancelled,
|
||||
RrdExportError,
|
||||
_recorded_blueprint,
|
||||
_recorded_view_points,
|
||||
_should_publish_recorded_point_frame,
|
||||
export_k1mqtt_to_rrd,
|
||||
recorded_blueprint_rrd,
|
||||
)
|
||||
from k1link.viewer.recorded import recorded_blueprint as viewer_recorded_blueprint
|
||||
from k1link.viewer.rerun_bridge import RerunSceneSettings
|
||||
|
||||
|
||||
@@ -45,6 +52,57 @@ def _point_payload(x: float) -> bytes:
|
||||
)
|
||||
|
||||
|
||||
def test_recorded_operator_projection_thins_only_dense_point_batches() -> None:
|
||||
assert RECORDED_VIEW_POINT_FRAME_STRIDE == 5
|
||||
count = RECORDED_VIEW_POINT_DECIMATION_THRESHOLD + 3
|
||||
positions = export_module.np.arange(
|
||||
count * 3,
|
||||
dtype=export_module.np.float32,
|
||||
).reshape((-1, 3))
|
||||
intensities = export_module.np.arange(count, dtype=export_module.np.uint8)
|
||||
rgb = export_module.np.arange(count * 3, dtype=export_module.np.uint8).reshape((-1, 3))
|
||||
|
||||
projected_positions, projected_intensities, projected_rgb = _recorded_view_points(
|
||||
positions,
|
||||
intensities,
|
||||
rgb,
|
||||
)
|
||||
|
||||
assert projected_rgb is not None
|
||||
assert export_module.np.array_equal(
|
||||
projected_positions,
|
||||
positions[::RECORDED_VIEW_POINT_STRIDE],
|
||||
)
|
||||
assert export_module.np.array_equal(
|
||||
projected_intensities,
|
||||
intensities[::RECORDED_VIEW_POINT_STRIDE],
|
||||
)
|
||||
assert export_module.np.array_equal(projected_rgb, rgb[::RECORDED_VIEW_POINT_STRIDE])
|
||||
|
||||
small_positions = positions[:RECORDED_VIEW_POINT_DECIMATION_THRESHOLD]
|
||||
small_intensities = intensities[:RECORDED_VIEW_POINT_DECIMATION_THRESHOLD]
|
||||
same_positions, same_intensities, no_rgb = _recorded_view_points(
|
||||
small_positions,
|
||||
small_intensities,
|
||||
None,
|
||||
)
|
||||
assert same_positions is small_positions
|
||||
assert same_intensities is small_intensities
|
||||
assert no_rgb is None
|
||||
|
||||
|
||||
def test_recorded_operator_projection_uses_stable_two_hz_point_cadence() -> None:
|
||||
published = [
|
||||
frame_number
|
||||
for frame_number in range(1, 13)
|
||||
if _should_publish_recorded_point_frame(frame_number)
|
||||
]
|
||||
|
||||
assert published == [1, 6, 11]
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
_should_publish_recorded_point_frame(0)
|
||||
|
||||
|
||||
def _pose_payload(x: float) -> bytes:
|
||||
return struct.pack("<ffffffff", x, 2.0, 3.0, 99.0, 1.0, 0.0, 0.0, 0.0)
|
||||
|
||||
@@ -191,8 +249,19 @@ def test_recorded_blueprint_accumulates_point_frames_on_session_timeline() -> No
|
||||
assert line_grid.visible.as_arrow_array().to_pylist() == [False]
|
||||
point_behavior, point_visualizer = spatial_view.visualizer_overrides["/world/points"]
|
||||
trajectory_behavior = spatial_view.visualizer_overrides["/world/trajectory"]
|
||||
perception_behavior = spatial_view.visualizer_overrides["/world/perception"]
|
||||
assert point_behavior.visible.as_arrow_array().to_pylist() == [False]
|
||||
assert trajectory_behavior.visible.as_arrow_array().to_pylist() == [True]
|
||||
assert perception_behavior.visible.as_arrow_array().to_pylist() == [False]
|
||||
perception_3d_view = blueprint.root_container.contents[2]
|
||||
assert perception_3d_view.origin == "/world"
|
||||
assert "VisibleTimeRanges" not in perception_3d_view.properties
|
||||
assert perception_3d_view.visualizer_overrides[
|
||||
"/world/perception"
|
||||
].visible.as_arrow_array().to_pylist() == [True]
|
||||
assert perception_3d_view.visualizer_overrides[
|
||||
"/world/perception/lidar"
|
||||
].visible.as_arrow_array().to_pylist() == [False]
|
||||
point_overrides = {
|
||||
str(batch.component_descriptor()): batch.as_arrow_array().to_pylist()
|
||||
for batch in point_visualizer.overrides
|
||||
@@ -235,8 +304,10 @@ def test_dynamic_blueprint_reuses_scene_ids_without_playback_mutation() -> None:
|
||||
assert first_view.id == second_view.id == RECORDED_SPATIAL_VIEW_ID
|
||||
assert first.root_container.contents[1].id == RECORDED_CAMERA_VIEW_ID
|
||||
assert second.root_container.contents[1].id == RECORDED_CAMERA_VIEW_ID
|
||||
assert first.root_container.contents[2].id == RECORDED_METRICS_VIEW_ID
|
||||
assert second.root_container.contents[2].id == RECORDED_METRICS_VIEW_ID
|
||||
assert first.root_container.contents[2].id == RECORDED_PERCEPTION_3D_VIEW_ID
|
||||
assert second.root_container.contents[2].id == RECORDED_PERCEPTION_3D_VIEW_ID
|
||||
assert first.root_container.contents[3].id == RECORDED_METRICS_VIEW_ID
|
||||
assert second.root_container.contents[3].id == RECORDED_METRICS_VIEW_ID
|
||||
|
||||
first_point_behavior, first_point_visualizer = first_view.visualizer_overrides["/world/points"]
|
||||
second_point_behavior, second_point_visualizer = second_view.visualizer_overrides[
|
||||
@@ -262,6 +333,107 @@ def test_dynamic_blueprint_reuses_scene_ids_without_playback_mutation() -> None:
|
||||
assert str(RECORDED_POINTS_VISUALIZER_ID).encode() in payload
|
||||
|
||||
|
||||
def test_viewer_blueprint_reset_is_bounded_and_recreates_render_views() -> None:
|
||||
initial = viewer_recorded_blueprint(
|
||||
RerunSceneSettings(accumulation_seconds=12.0),
|
||||
include_initial_playback_state=False,
|
||||
active_view="perception",
|
||||
view_reset_generation=0,
|
||||
)
|
||||
reset = viewer_recorded_blueprint(
|
||||
RerunSceneSettings(accumulation_seconds=12.0),
|
||||
include_initial_playback_state=False,
|
||||
active_view="perception",
|
||||
view_reset_generation=1,
|
||||
)
|
||||
restored = viewer_recorded_blueprint(
|
||||
RerunSceneSettings(accumulation_seconds=12.0),
|
||||
include_initial_playback_state=False,
|
||||
active_view="perception",
|
||||
view_reset_generation=0,
|
||||
)
|
||||
|
||||
assert initial.root_container.id == restored.root_container.id
|
||||
assert reset.root_container.id != initial.root_container.id
|
||||
assert initial.root_container.contents[0].id != reset.root_container.contents[0].id
|
||||
assert restored.root_container.contents[0].id == initial.root_container.contents[0].id
|
||||
assert initial.root_container.active_tab == 1
|
||||
assert reset.root_container.active_tab == 1
|
||||
assert initial.root_container.contents[1].id != reset.root_container.contents[1].id
|
||||
assert initial.root_container.contents[2].id != reset.root_container.contents[2].id
|
||||
assert restored.root_container.contents[1].id == initial.root_container.contents[1].id
|
||||
assert restored.root_container.contents[2].id == initial.root_container.contents[2].id
|
||||
cuboids = viewer_recorded_blueprint(
|
||||
RerunSceneSettings(accumulation_seconds=12.0),
|
||||
include_initial_playback_state=False,
|
||||
active_view="perception3d",
|
||||
)
|
||||
assert cuboids.root_container.active_tab == 2
|
||||
assert cuboids.root_container.id != initial.root_container.id
|
||||
cuboid_view = cuboids.root_container.contents[2]
|
||||
assert cuboid_view.origin == "/world"
|
||||
assert "VisibleTimeRanges" not in cuboid_view.properties
|
||||
cuboid_points, cuboid_point_visualizer = cuboid_view.visualizer_overrides[
|
||||
"/world/points"
|
||||
]
|
||||
cuboid_perception = cuboid_view.visualizer_overrides["/world/perception"]
|
||||
cuboid_diagnostic_lidar = cuboid_view.visualizer_overrides[
|
||||
"/world/perception/lidar"
|
||||
]
|
||||
assert cuboid_points.visible.as_arrow_array().to_pylist() == [True]
|
||||
assert cuboid_point_visualizer.id != initial.root_container.contents[0].visualizer_overrides[
|
||||
"/world/points"
|
||||
][1].id
|
||||
assert cuboid_perception.visible.as_arrow_array().to_pylist() == [True]
|
||||
assert cuboid_diagnostic_lidar.visible.as_arrow_array().to_pylist() == [False]
|
||||
perception_behavior = initial.root_container.contents[0].visualizer_overrides[
|
||||
"/world/perception"
|
||||
]
|
||||
assert perception_behavior.visible.as_arrow_array().to_pylist() == [False]
|
||||
|
||||
|
||||
def test_viewer_blueprint_unifies_original_video_and_independent_ai_layers() -> None:
|
||||
blueprint = viewer_recorded_blueprint(
|
||||
RerunSceneSettings(accumulation_seconds=12.0),
|
||||
include_initial_playback_state=False,
|
||||
unified_perception=True,
|
||||
show_detections_2d=True,
|
||||
show_segmentation=True,
|
||||
show_cuboids_3d=False,
|
||||
)
|
||||
|
||||
assert type(blueprint.root_container).__name__ == "Horizontal"
|
||||
camera_view, spatial_view = blueprint.root_container.contents
|
||||
assert camera_view.origin == "/perception/camera"
|
||||
assert spatial_view.origin == "/world"
|
||||
assert camera_view.visualizer_overrides[
|
||||
"/perception/camera/image"
|
||||
].visible.as_arrow_array().to_pylist() == [True]
|
||||
assert camera_view.visualizer_overrides[
|
||||
"/perception/camera/detections"
|
||||
].visible.as_arrow_array().to_pylist() == [True]
|
||||
assert camera_view.visualizer_overrides[
|
||||
"/perception/camera/segmentation"
|
||||
].visible.as_arrow_array().to_pylist() == [True]
|
||||
semantic_behavior, semantic_time_ranges = spatial_view.visualizer_overrides[
|
||||
"/world/perception/semantic_points"
|
||||
]
|
||||
cuboid_behavior, cuboid_time_ranges = spatial_view.visualizer_overrides[
|
||||
"/world/perception/boxes3d"
|
||||
]
|
||||
assert semantic_behavior.visible.as_arrow_array().to_pylist() == [True]
|
||||
assert cuboid_behavior.visible.as_arrow_array().to_pylist() == [False]
|
||||
assert semantic_time_ranges.ranges.as_arrow_array().to_pylist() == []
|
||||
assert cuboid_time_ranges.ranges.as_arrow_array().to_pylist() == []
|
||||
visible_ranges = spatial_view.properties["VisibleTimeRanges"]
|
||||
assert visible_ranges.ranges.as_arrow_array().to_pylist() == [
|
||||
{
|
||||
"timeline": SESSION_TIMELINE,
|
||||
"range": {"start": -12_000_000_000, "end": 0},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_export_preserves_every_decodable_frame_and_source_timeline(tmp_path: Path) -> None:
|
||||
capture = _write_capture(
|
||||
tmp_path,
|
||||
|
||||
+233
-17
@@ -12,10 +12,11 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, HTTPException, Response
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
import k1link.sessions.media as recorded_media_module
|
||||
from k1link.compute import RecordedPerceptionVideo
|
||||
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 (
|
||||
@@ -166,9 +167,24 @@ async def render_file_response(
|
||||
*,
|
||||
range_header: bytes | None,
|
||||
) -> tuple[dict[str, Any], bytes]:
|
||||
body_iterator = getattr(response, "body_iterator", None)
|
||||
if body_iterator is not None:
|
||||
chunks = [chunk async for chunk in body_iterator]
|
||||
return {
|
||||
"status": response.status_code,
|
||||
"headers": response.raw_headers,
|
||||
}, b"".join(
|
||||
chunk.encode("utf-8") if isinstance(chunk, str) else chunk
|
||||
for chunk in chunks
|
||||
)
|
||||
sent: list[dict[str, Any]] = []
|
||||
request_delivered = False
|
||||
|
||||
async def receive() -> dict[str, Any]:
|
||||
nonlocal request_delivered
|
||||
if request_delivered:
|
||||
return {"type": "http.disconnect"}
|
||||
request_delivered = True
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
async def send(message: dict[str, Any]) -> None:
|
||||
@@ -1069,6 +1085,11 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
|
||||
palette="custom",
|
||||
custom_color="#112233",
|
||||
active_view="perception",
|
||||
view_reset_generation=1,
|
||||
unified_perception=True,
|
||||
show_detections_2d=True,
|
||||
show_segmentation=True,
|
||||
show_cuboids_3d=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -1083,6 +1104,11 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
|
||||
assert observed_settings[0].show_points is False
|
||||
assert observed_settings[0].show_trajectory is True
|
||||
assert observed_kwargs[0]["active_view"] == "perception"
|
||||
assert observed_kwargs[0]["view_reset_generation"] == 1
|
||||
assert observed_kwargs[0]["unified_perception"] is True
|
||||
assert observed_kwargs[0]["show_detections_2d"] is True
|
||||
assert observed_kwargs[0]["show_segmentation"] is True
|
||||
assert observed_kwargs[0]["show_cuboids_3d"] is True
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
RecordedBlueprintRequest.model_validate(
|
||||
@@ -1234,6 +1260,12 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
"epochs/{epoch_ordinal}/segments/{segment_sequence}.m4s",
|
||||
"GET",
|
||||
)
|
||||
stream_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/media/{artifact_id}/"
|
||||
"epochs/{epoch_ordinal}/recording.mp4",
|
||||
"GET",
|
||||
)
|
||||
|
||||
replay = asyncio.run(replay_route(session_id=session.name, request=None))
|
||||
source = replay["launch"]["media_sources"][0]
|
||||
@@ -1271,39 +1303,66 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
if_match=f'"sha256:{source["manifest_generation_sha256"]}"',
|
||||
)
|
||||
manifest = json.loads(manifest_response.body)
|
||||
assert manifest["schema_version"] == "missioncore.observation-recorded-media/v2"
|
||||
assert manifest["schema_version"] == "missioncore.observation-recorded-media/v3"
|
||||
assert manifest["source_id"] == source["id"]
|
||||
assert manifest["generation_sha256"] == source["manifest_generation_sha256"]
|
||||
assert manifest["byte_length"] == source["byte_length"]
|
||||
assert manifest["timeline_start_seconds"] == source["timeline_start_seconds"]
|
||||
assert manifest["timeline_end_seconds"] == source["timeline_end_seconds"]
|
||||
assert manifest_response.headers["etag"] == (f'"sha256:{manifest["generation_sha256"]}"')
|
||||
init_sha256 = hashlib.sha256(init).hexdigest()
|
||||
segment_sha256 = hashlib.sha256(segment).hexdigest()
|
||||
generation = manifest["generation_sha256"]
|
||||
assert manifest["epochs"] == [
|
||||
{
|
||||
"ordinal": 1,
|
||||
"timeline_start_seconds": 0.0,
|
||||
"timeline_end_seconds": 0.5,
|
||||
"media_type": 'video/mp4; codecs="avc1.640028"',
|
||||
"init_url": manifest["epochs"][0]["init_url"],
|
||||
"init_byte_length": len(init),
|
||||
"init_sha256": init_sha256,
|
||||
"segment_count": 1,
|
||||
"segment_url_prefix": manifest["epochs"][0]["segment_url_prefix"],
|
||||
"segments": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"url": manifest["epochs"][0]["segments"][0]["url"],
|
||||
"byte_length": len(segment),
|
||||
"sha256": segment_sha256,
|
||||
}
|
||||
],
|
||||
"byte_length": len(init) + len(segment),
|
||||
"stream_url": (
|
||||
f"{source['manifest_url'].removesuffix('/manifest')}/epochs/1/recording.mp4"
|
||||
f"?generation={generation}"
|
||||
),
|
||||
}
|
||||
]
|
||||
assert "sensor.camera.private-left" not in json.dumps(manifest)
|
||||
assert_no_local_paths(manifest, repository)
|
||||
|
||||
request = Request({"type": "http", "method": "GET", "headers": []})
|
||||
with pytest.raises(HTTPException) as stale_stream_generation:
|
||||
stream_route(
|
||||
request=request,
|
||||
session_id=session.name,
|
||||
artifact_id=artifact_id,
|
||||
epoch_ordinal=1,
|
||||
generation="0" * 64,
|
||||
range_header="bytes=0-5",
|
||||
)
|
||||
assert stale_stream_generation.value.status_code == 412
|
||||
|
||||
stream_response = stream_route(
|
||||
request=request,
|
||||
session_id=session.name,
|
||||
artifact_id=artifact_id,
|
||||
epoch_ordinal=1,
|
||||
generation=generation,
|
||||
range_header=f"bytes={len(init) - 2}-{len(init) + 3}",
|
||||
)
|
||||
stream_start, stream_body = asyncio.run(
|
||||
render_file_response(stream_response, range_header=None)
|
||||
)
|
||||
stream_headers = {
|
||||
key.decode("latin-1"): value.decode("latin-1")
|
||||
for key, value in stream_start["headers"]
|
||||
}
|
||||
assert stream_start["status"] == 206
|
||||
assert stream_body == init[-2:] + segment[:4]
|
||||
assert stream_headers["content-range"] == (
|
||||
f"bytes {len(init) - 2}-{len(init) + 3}/{len(init) + len(segment)}"
|
||||
)
|
||||
assert stream_headers["content-length"] == "6"
|
||||
assert stream_headers["accept-ranges"] == "bytes"
|
||||
assert stream_headers["etag"] == f'"generation:{generation}:epoch:1"'
|
||||
|
||||
with pytest.raises(HTTPException) as missing_init_generation:
|
||||
init_route(
|
||||
session_id=session.name,
|
||||
@@ -1313,6 +1372,9 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
)
|
||||
assert missing_init_generation.value.status_code == 428
|
||||
|
||||
init_sha256 = hashlib.sha256(init).hexdigest()
|
||||
segment_sha256 = hashlib.sha256(segment).hexdigest()
|
||||
|
||||
init_response = init_route(
|
||||
session_id=session.name,
|
||||
artifact_id=artifact_id,
|
||||
@@ -1375,6 +1437,160 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
assert confined.value.status_code == 409
|
||||
|
||||
|
||||
def test_replay_exposes_generation_bound_perception_video_with_native_ranges(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260720T065719Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
recording_payload = b"recording"
|
||||
|
||||
def export_recording(source: Path, destination: Path) -> dict[str, object]:
|
||||
destination.write_bytes(recording_payload)
|
||||
return {
|
||||
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
"rrd_sha256": hashlib.sha256(recording_payload).hexdigest(),
|
||||
"rrd_bytes": len(recording_payload),
|
||||
"timeline": "session_time",
|
||||
"timeline_start_ns": 0,
|
||||
"timeline_end_ns": 5_000_000_000,
|
||||
}
|
||||
|
||||
video_payload = b"sealed-panoptic-video"
|
||||
video_path = tmp_path / "perception.mp4"
|
||||
video_path.write_bytes(video_payload)
|
||||
video_sha256 = hashlib.sha256(video_payload).hexdigest()
|
||||
result_id = f"result-{'d' * 64}"
|
||||
video = RecordedPerceptionVideo(
|
||||
result_id=result_id,
|
||||
session_id=session.name,
|
||||
source_id="sensor.camera.right",
|
||||
public_source_id="recorded.perception.right",
|
||||
label="Сегментация · камера right",
|
||||
path=video_path,
|
||||
media_type='video/mp4; codecs="avc1.640028"',
|
||||
byte_length=len(video_payload),
|
||||
sha256=video_sha256,
|
||||
timeline_start_seconds=0.5,
|
||||
timeline_end_seconds=4.5,
|
||||
)
|
||||
|
||||
class Provider:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, str | None]] = []
|
||||
|
||||
def video(
|
||||
self,
|
||||
session_id: str,
|
||||
requested_result_id: str | None = None,
|
||||
) -> RecordedPerceptionVideo | None:
|
||||
self.calls.append((session_id, requested_result_id))
|
||||
if session_id != video.session_id:
|
||||
return None
|
||||
if requested_result_id is not None and requested_result_id != video.result_id:
|
||||
return None
|
||||
return video
|
||||
|
||||
provider = Provider()
|
||||
router = build_session_router(
|
||||
store,
|
||||
recording_materializer=SessionRecordingMaterializer(
|
||||
store.data_dir,
|
||||
exporter=export_recording,
|
||||
),
|
||||
perception_media_provider=provider,
|
||||
allow_synchronous_recording_fallback=True,
|
||||
)
|
||||
replay_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/replay",
|
||||
"POST",
|
||||
)
|
||||
manifest_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/perception-media/"
|
||||
"{result_id}/manifest",
|
||||
"GET",
|
||||
)
|
||||
stream_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/perception-media/"
|
||||
"{result_id}/recording.mp4",
|
||||
"GET",
|
||||
)
|
||||
|
||||
replay = asyncio.run(replay_route(session_id=session.name, request=None))
|
||||
# Perception is loaded through the on-demand RRD overlay and no longer
|
||||
# blocks replay launch by hashing a duplicate pre-rendered video.
|
||||
assert replay["launch"]["media_sources"] == []
|
||||
assert provider.calls == []
|
||||
|
||||
with pytest.raises(HTTPException) as missing_generation:
|
||||
manifest_route(session_id=session.name, result_id=result_id)
|
||||
assert missing_generation.value.status_code == 428
|
||||
manifest_response = manifest_route(
|
||||
session_id=session.name,
|
||||
result_id=result_id,
|
||||
if_match=f'"sha256:{video_sha256}"',
|
||||
)
|
||||
assert provider.calls == [
|
||||
(session.name, result_id),
|
||||
(session.name, result_id),
|
||||
]
|
||||
manifest = json.loads(manifest_response.body)
|
||||
assert manifest == {
|
||||
"schema_version": "missioncore.observation-recorded-media/v3",
|
||||
"source_id": "recorded.perception.right",
|
||||
"generation_sha256": video_sha256,
|
||||
"byte_length": len(video_payload),
|
||||
"timeline_start_seconds": 0.5,
|
||||
"timeline_end_seconds": 4.5,
|
||||
"synchronization": "host-arrival-best-effort",
|
||||
"epochs": [
|
||||
{
|
||||
"ordinal": 1,
|
||||
"timeline_start_seconds": 0.5,
|
||||
"timeline_end_seconds": 4.5,
|
||||
"media_type": 'video/mp4; codecs="avc1.640028"',
|
||||
"byte_length": len(video_payload),
|
||||
"stream_url": (
|
||||
f"/api/v1/observation-sessions/{session.name}/perception-media/"
|
||||
f"{result_id}/recording.mp4?generation={video_sha256}"
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
request = Request({"type": "http", "method": "GET", "headers": []})
|
||||
with pytest.raises(HTTPException) as stale_generation:
|
||||
stream_route(
|
||||
request=request,
|
||||
session_id=session.name,
|
||||
result_id=result_id,
|
||||
generation="0" * 64,
|
||||
range_header=None,
|
||||
)
|
||||
assert stale_generation.value.status_code == 412
|
||||
response = stream_route(
|
||||
request=request,
|
||||
session_id=session.name,
|
||||
result_id=result_id,
|
||||
generation=video_sha256,
|
||||
range_header="bytes=7-14",
|
||||
)
|
||||
start, body = asyncio.run(render_file_response(response, range_header=b"bytes=7-14"))
|
||||
headers = {
|
||||
key.decode("latin-1"): value.decode("latin-1") for key, value in start["headers"]
|
||||
}
|
||||
assert start["status"] == 206
|
||||
assert body == video_payload[7:15]
|
||||
assert headers["content-range"] == f"bytes 7-14/{len(video_payload)}"
|
||||
assert headers["accept-ranges"] == "bytes"
|
||||
assert headers["etag"] == f'"sha256:{video_sha256}"'
|
||||
|
||||
|
||||
def test_ready_launch_uses_background_prepared_camera_manifest_without_rescan(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@@ -16,6 +16,8 @@ import k1link.sessions.recording as recording_module
|
||||
from k1link.sessions.models import ReplayArtifact, ReplayCommand
|
||||
from k1link.sessions.recording import (
|
||||
CACHE_SCHEMA,
|
||||
RECORDING_CACHE_FILENAME,
|
||||
RECORDING_CACHE_SIDECAR_FILENAME,
|
||||
RERUN_RECORDING_MEDIA_TYPE,
|
||||
RecordingMaterializationError,
|
||||
SessionRecordingMaterializer,
|
||||
@@ -113,7 +115,7 @@ def test_materializer_reuses_only_a_digest_validated_private_cache(tmp_path: Pat
|
||||
assert first.timeline_end_ns == 2_500_000_000
|
||||
assert first.path.is_relative_to(materializer.recordings_root)
|
||||
assert first.path.stat().st_mode & 0o777 == 0o600
|
||||
sidecar = first.path.with_name("scene.rrd.cache.json")
|
||||
sidecar = first.path.with_name(RECORDING_CACHE_SIDECAR_FILENAME)
|
||||
assert sidecar.stat().st_mode & 0o777 == 0o600
|
||||
document = json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
assert document["schema_version"] == CACHE_SCHEMA
|
||||
@@ -129,6 +131,20 @@ def test_materializer_reuses_only_a_digest_validated_private_cache(tmp_path: Pat
|
||||
assert exporter.calls == 1
|
||||
|
||||
|
||||
def test_default_recording_cache_has_no_application_byte_quota(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.delenv("MISSIONCORE_RRD_CACHE_MAX_BYTES", raising=False)
|
||||
|
||||
materializer = SessionRecordingMaterializer(
|
||||
tmp_path / "private",
|
||||
exporter=FakeExporter(),
|
||||
)
|
||||
|
||||
assert materializer.cache_max_bytes is None
|
||||
|
||||
|
||||
def test_delete_cached_refuses_a_live_lease_then_removes_the_exact_cache(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -199,7 +215,7 @@ def test_materializer_rebuilds_incompatible_recording_cache_schema(
|
||||
exporter = FakeExporter()
|
||||
private_root = tmp_path / "private"
|
||||
first = SessionRecordingMaterializer(private_root, exporter=exporter).materialize(command)
|
||||
sidecar = first.path.with_name("scene.rrd.cache.json")
|
||||
sidecar = first.path.with_name(RECORDING_CACHE_SIDECAR_FILENAME)
|
||||
document = json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
document["schema_version"] = obsolete_schema
|
||||
sidecar.write_text(json.dumps(document), encoding="utf-8")
|
||||
@@ -217,7 +233,7 @@ def test_failed_rebuild_preserves_previously_published_cache(tmp_path: Path) ->
|
||||
materializer = SessionRecordingMaterializer(tmp_path / "private", exporter=exporter)
|
||||
first = materializer.materialize(command)
|
||||
published_bytes = first.path.read_bytes()
|
||||
sidecar = first.path.with_name("scene.rrd.cache.json")
|
||||
sidecar = first.path.with_name(RECORDING_CACHE_SIDECAR_FILENAME)
|
||||
published_sidecar = sidecar.read_bytes()
|
||||
command.primary_artifact.path.write_bytes(b"new-source-that-requires-rebuild")
|
||||
changed = _replace_primary(
|
||||
@@ -300,9 +316,9 @@ def test_materializer_never_follows_cache_symlinks_outside_private_root(
|
||||
assert list(outside.iterdir()) == []
|
||||
session_cache.unlink()
|
||||
session_cache.mkdir()
|
||||
outside_recording = outside / "scene.rrd"
|
||||
outside_recording = outside / RECORDING_CACHE_FILENAME
|
||||
outside_recording.write_bytes(b"do-not-touch")
|
||||
(session_cache / "scene.rrd").symlink_to(outside_recording)
|
||||
(session_cache / RECORDING_CACHE_FILENAME).symlink_to(outside_recording)
|
||||
|
||||
recording = materializer.materialize(command)
|
||||
|
||||
|
||||
@@ -566,6 +566,60 @@ def test_interrupted_capture_is_recovered_from_aligned_raw_and_metadata_prefix(
|
||||
assert store.prepare_replay(session.name).primary_artifact.path.name == "mqtt.raw.k1mqtt"
|
||||
|
||||
|
||||
def test_interrupted_capture_streams_metadata_beyond_legacy_total_limit(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
capture = session / "captures" / "mqtt_live"
|
||||
raw = bytearray(RAW_MAGIC)
|
||||
records: list[bytes] = []
|
||||
topic = "lixel/application/report/lio_pose"
|
||||
topic_bytes = topic.encode()
|
||||
padding = "x" * (60 * 1024)
|
||||
for sequence in range(1, 1_101):
|
||||
payload = b"p"
|
||||
frame_offset = len(raw)
|
||||
raw.extend(FRAME_HEADER.pack(len(topic_bytes), len(payload)))
|
||||
raw.extend(topic_bytes)
|
||||
raw.extend(payload)
|
||||
record = {
|
||||
"schema_version": 1,
|
||||
"record_type": "message",
|
||||
"sequence": sequence,
|
||||
"received_at_utc": "2026-07-16T20:56:32.699Z",
|
||||
"received_at_epoch_ns": 1_784_235_391_699_000_000 + sequence,
|
||||
"received_monotonic_ns": 9_000_000_000 + sequence,
|
||||
"topic": topic,
|
||||
"payload_bytes": len(payload),
|
||||
"raw_frame_offset": frame_offset,
|
||||
"raw_payload_offset": frame_offset + FRAME_HEADER.size + len(topic_bytes),
|
||||
"raw_frame_bytes": FRAME_HEADER.size + len(topic_bytes) + len(payload),
|
||||
"padding": padding,
|
||||
}
|
||||
records.append((json.dumps(record, separators=(",", ":")) + "\n").encode())
|
||||
(capture / "mqtt.raw.k1mqtt").write_bytes(raw)
|
||||
metadata_path = capture / "mqtt.metadata.jsonl"
|
||||
metadata_path.write_bytes(b"".join(records))
|
||||
assert metadata_path.stat().st_size > 64 * 1024 * 1024
|
||||
(capture / "mqtt.summary.json").write_text("{corrupt", encoding="utf-8")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "interrupted"
|
||||
assert detail.summary.replayable is True
|
||||
command = store.prepare_replay(session.name)
|
||||
assert command.primary_artifact.replay_byte_length == len(raw)
|
||||
assert next(
|
||||
artifact.replay_byte_length
|
||||
for artifact in command.artifacts
|
||||
if artifact.artifact_id == "raw-transport-index"
|
||||
) == metadata_path.stat().st_size
|
||||
|
||||
|
||||
def test_catalog_upsert_promotes_recovered_session_after_summary_is_completed(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -72,4 +72,7 @@ def test_viewer_settings_are_validated_and_exposed(tmp_path: Path) -> None:
|
||||
"show_points": True,
|
||||
"show_trajectory": False,
|
||||
"show_grid": False,
|
||||
"show_detections_2d": False,
|
||||
"show_segmentation": False,
|
||||
"show_cuboids_3d": False,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
import k1link.device_plugins.xgrids_k1.ble.wifi_provisioning as wifi_module
|
||||
from k1link.device_plugins.xgrids_k1.ble.wifi_provisioning import (
|
||||
FRAME_LENGTH,
|
||||
build_wifi_provisioning_frame,
|
||||
parse_wifi_status,
|
||||
read_wifi_status_once,
|
||||
)
|
||||
|
||||
|
||||
@@ -70,3 +76,55 @@ def test_parse_wifi_status_ap_baseline() -> None:
|
||||
def test_parse_wifi_status_rejects_short_frame() -> None:
|
||||
with pytest.raises(ValueError, match="at least 51 bytes"):
|
||||
parse_wifi_status(bytes(50))
|
||||
|
||||
|
||||
def test_read_wifi_status_once_reads_only_and_returns_current_dhcp_address(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
value = bytearray(54)
|
||||
value[0] = 11
|
||||
value[1:12] = b"WIFI_CLIENT"
|
||||
value[33] = 4
|
||||
value[34:38] = bytes((10, 255, 254, 77))
|
||||
value[50] = 1
|
||||
characteristic = SimpleNamespace(
|
||||
uuid=wifi_module.STATUS_CHARACTERISTIC_UUID,
|
||||
service_uuid=wifi_module.SERVICE_UUID,
|
||||
properties=["read"],
|
||||
)
|
||||
service = SimpleNamespace(uuid=wifi_module.SERVICE_UUID)
|
||||
|
||||
class FakeServices:
|
||||
def get_service(self, uuid: str) -> object | None:
|
||||
return service if uuid == wifi_module.SERVICE_UUID else None
|
||||
|
||||
def get_characteristic(self, uuid: str) -> object | None:
|
||||
return characteristic if uuid == wifi_module.STATUS_CHARACTERISTIC_UUID else None
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, _device: object, **_kwargs: object) -> None:
|
||||
self.services = FakeServices()
|
||||
self.name = "XGR-K1"
|
||||
self.write_calls = 0
|
||||
|
||||
async def __aenter__(self) -> Any:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def read_gatt_char(self, _characteristic: object) -> bytes:
|
||||
return bytes(value)
|
||||
|
||||
async def write_gatt_char(self, *_args: object, **_kwargs: object) -> None:
|
||||
self.write_calls += 1
|
||||
raise AssertionError("status refresh must not write a BLE characteristic")
|
||||
|
||||
monkeypatch.setattr(wifi_module, "discovered_device", lambda _uuid: object())
|
||||
monkeypatch.setattr(wifi_module, "BleakClient", FakeClient)
|
||||
|
||||
result = asyncio.run(read_wifi_status_once("synthetic-corebluetooth-uuid"))
|
||||
|
||||
assert result["operation"] == "single_reviewed_wifi_status_read"
|
||||
assert result["write_performed"] is False
|
||||
assert result["status"]["ipv4"] == "10.255.254.77"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -64,7 +67,7 @@ class FakeVisualizationRuntime:
|
||||
self.source_mode = "idle"
|
||||
self.source_ready = False
|
||||
self.pcl_frames = 0
|
||||
self.start_calls: list[tuple[str, Path, float, str]] = []
|
||||
self.start_calls: list[tuple[str, Path, float | None, str]] = []
|
||||
self.stop_calls = 0
|
||||
self.stop_error: Exception | None = None
|
||||
|
||||
@@ -102,7 +105,7 @@ class FakeVisualizationRuntime:
|
||||
host: str,
|
||||
out_dir: Path,
|
||||
*,
|
||||
duration_seconds: float,
|
||||
duration_seconds: float | None,
|
||||
project_name: str,
|
||||
) -> None:
|
||||
self.start_calls.append((host, out_dir, duration_seconds, project_name))
|
||||
@@ -195,6 +198,132 @@ def service_with_fake_runtime(
|
||||
return service, runtime
|
||||
|
||||
|
||||
def _wifi_status_read(ipv4: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
||||
"observed_at_utc": "2026-07-20T12:00:00Z",
|
||||
"adapter": "CoreBluetooth",
|
||||
"bleak_version": "test",
|
||||
"device_macos_uuid": "test-ble-transport",
|
||||
"device_name": "XGR-K1",
|
||||
"service_uuid": "00007f00-0000-1000-8000-00805f9b34fb",
|
||||
"status_characteristic_uuid": "00007f02-0000-1000-8000-00805f9b34fb",
|
||||
"operation": "single_reviewed_wifi_status_read",
|
||||
"write_performed": False,
|
||||
"status": {
|
||||
"value_length": 54,
|
||||
"mode": "WIFI_CLIENT",
|
||||
"ipv4": ipv4,
|
||||
"status_code": 1,
|
||||
"reserved": 0,
|
||||
"trailer_hex": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_verify_connection_refreshes_dynamic_dhcp_address_and_rotates_session(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
||||
service._connection_mode = "bridge" # noqa: SLF001
|
||||
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
||||
service._device_id = "known-k1" # noqa: SLF001
|
||||
service._device_session_id = "old-device-session" # noqa: SLF001
|
||||
service._device_session_opened_at = "2026-07-20T10:00:00Z" # noqa: SLF001
|
||||
service._device_calibration = {"status": "available"} # noqa: SLF001
|
||||
|
||||
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
||||
return _wifi_status_read("10.255.254.77")
|
||||
|
||||
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
||||
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
||||
|
||||
state = service.verify_connection()
|
||||
|
||||
assert state["k1_ip"] == "10.255.254.77"
|
||||
assert state["device_session"]["device_session_id"] != "old-device-session"
|
||||
assert state["connection_verification"] == {
|
||||
"status": "live-address-observed",
|
||||
"endpoint_validation": "ble-wifi-status-read",
|
||||
"network_reachability": "not-probed",
|
||||
"address_changed": True,
|
||||
"previous_address_present": True,
|
||||
"write_performed": False,
|
||||
"observed_at": "2026-07-20T12:00:00Z",
|
||||
}
|
||||
assert state["device_calibration"]["status"] == "unavailable"
|
||||
|
||||
|
||||
def test_implicit_acquisition_target_uses_current_ble_dhcp_address(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
||||
service._connection_mode = "bridge" # noqa: SLF001
|
||||
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
||||
|
||||
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
||||
return _wifi_status_read("10.255.254.77")
|
||||
|
||||
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
||||
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
||||
|
||||
state = service.prepare_acquisition(
|
||||
PrepareAcquisitionRequest(
|
||||
project_name=PROJECT_NAME,
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
)
|
||||
|
||||
assert state["k1_ip"] == "10.255.254.77"
|
||||
assert state["acquisition"]["target_host"] == "10.255.254.77"
|
||||
|
||||
|
||||
def test_control_session_opens_against_current_ble_dhcp_address(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._selected_device_id = "test-ble-transport" # noqa: SLF001
|
||||
service._connection_mode = "bridge" # noqa: SLF001
|
||||
service._k1_ip = "10.255.254.54" # noqa: SLF001
|
||||
service._compatibility_attestation = {"profile": "exact"} # noqa: SLF001
|
||||
opened_hosts: list[str] = []
|
||||
|
||||
class FakeOpenControlSession:
|
||||
def snapshot(self) -> dict[str, object]:
|
||||
return {"state": "idle", "can_confirm_standby": False}
|
||||
|
||||
def open(self, *, host: str, **_: object) -> dict[str, object]:
|
||||
opened_hosts.append(host)
|
||||
return self.snapshot()
|
||||
|
||||
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
||||
return _wifi_status_read("10.255.254.77")
|
||||
|
||||
service._application_control_session = FakeOpenControlSession() # type: ignore[assignment] # noqa: SLF001
|
||||
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
||||
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
||||
|
||||
state = service.open_application_control_session(
|
||||
OpenApplicationControlSessionRequest(
|
||||
operator_present=True,
|
||||
owner_controlled_device=True,
|
||||
lixelgo_closed=True,
|
||||
battery_storage_confirmed=True,
|
||||
expected_physical_state_confirmed=True,
|
||||
timezone_name="Europe/Moscow",
|
||||
)
|
||||
)
|
||||
|
||||
assert opened_hosts == ["10.255.254.77"]
|
||||
assert state["k1_ip"] == "10.255.254.77"
|
||||
|
||||
|
||||
def test_prepare_creates_provisional_device_session_and_profiled_acquisition(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -237,11 +366,26 @@ def test_project_name_is_normalized_and_control_characters_are_rejected() -> Non
|
||||
)
|
||||
|
||||
|
||||
def test_acquisition_is_unbounded_by_default_and_accepts_ten_hour_hint() -> None:
|
||||
unbounded = PrepareAcquisitionRequest(
|
||||
project_name=PROJECT_NAME,
|
||||
host="192.168.1.20",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
ten_hours = PrepareAcquisitionRequest(
|
||||
project_name=PROJECT_NAME,
|
||||
host="192.168.1.20",
|
||||
duration_seconds=10 * 60 * 60,
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
|
||||
assert unbounded.duration_seconds is None
|
||||
assert ten_hours.duration_seconds == 36_000
|
||||
|
||||
|
||||
def test_connection_modes_require_their_exact_topology_attestation() -> None:
|
||||
assert ConnectRequest(
|
||||
device_id="synthetic-device",
|
||||
ssid="synthetic-network",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
).connection_mode == "quick-connect"
|
||||
@@ -253,12 +397,18 @@ def test_connection_modes_require_their_exact_topology_attestation() -> None:
|
||||
compatibility_attestation=DIRECT_CONNECT_ATTESTATION,
|
||||
).connection_mode == "direct-connect"
|
||||
with pytest.raises(ValidationError):
|
||||
ConnectRequest(
|
||||
device_id="synthetic-device",
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
with pytest.raises(ValidationError, match="host Wi-Fi profile"):
|
||||
ConnectRequest(
|
||||
device_id="synthetic-device",
|
||||
ssid="synthetic-network",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
with pytest.raises(ValidationError, match="32 UTF-8 bytes"):
|
||||
ConnectRequest(
|
||||
@@ -790,10 +940,10 @@ def test_camera_arm_failure_seals_stopped_session_before_releasing_lease(
|
||||
_host: str,
|
||||
out_dir: Path,
|
||||
*,
|
||||
duration_seconds: float,
|
||||
duration_seconds: float | None,
|
||||
project_name: str,
|
||||
) -> None:
|
||||
assert duration_seconds > 0
|
||||
assert duration_seconds is None
|
||||
assert project_name == PROJECT_NAME
|
||||
events.append("start")
|
||||
out_dir.mkdir(parents=True)
|
||||
@@ -1855,50 +2005,97 @@ def test_network_provisioning_is_single_flight_and_secret_is_unwrapped_only_at_b
|
||||
assert {item["status"] for item in provision_operations} == {"succeeded", "failed"}
|
||||
|
||||
|
||||
def test_quick_connect_associates_the_host_without_a_ble_write(
|
||||
def test_quick_connect_activates_the_device_ap_then_associates_the_host(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
|
||||
service._devices = [{"device_id": "k1-a", "name": "XGR-TEST-A"}] # noqa: SLF001
|
||||
activation_calls: list[str] = []
|
||||
association_calls: list[tuple[Path, str, str]] = []
|
||||
ble_session_open = False
|
||||
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"ensure_wifi_profile_from_credential_source",
|
||||
lambda *_args, **_kwargs: {
|
||||
"schema_version": 1,
|
||||
"adapter": "macOS Keychain",
|
||||
"available": True,
|
||||
"profile_enrolled": True,
|
||||
"credential_source": "exact-firmware-profile",
|
||||
},
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def fake_activation_session(
|
||||
device_id: str, **_: object
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
nonlocal ble_session_open
|
||||
activation_calls.append(device_id)
|
||||
ble_session_open = True
|
||||
try:
|
||||
yield {
|
||||
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
||||
"started_at_utc": "2026-07-19T15:00:00Z",
|
||||
"completed_at_utc": "2026-07-19T15:00:01Z",
|
||||
"outcome": "ap_ready_observed",
|
||||
"ready_observed": True,
|
||||
"write_performed": True,
|
||||
"write_mode": "with_response",
|
||||
}
|
||||
finally:
|
||||
ble_session_open = False
|
||||
|
||||
def fake_associate(
|
||||
helper_path: Path,
|
||||
ssid: str,
|
||||
password: str,
|
||||
profile_id: str,
|
||||
expected_ssid: str,
|
||||
**_: object,
|
||||
) -> dict[str, Any]:
|
||||
association_calls.append((helper_path, ssid, password))
|
||||
assert ble_session_open
|
||||
association_calls.append((helper_path, profile_id, expected_ssid))
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"adapter": "CoreWLAN",
|
||||
"outcome": "associated",
|
||||
"already_associated": False,
|
||||
"profile_enrolled": True,
|
||||
"scan_attempt_count": 2,
|
||||
"scan_elapsed_ms": 900,
|
||||
"credential_source": "system-wifi-keychain",
|
||||
}
|
||||
|
||||
async def forbidden_ble_write(*_: object, **__: object) -> dict[str, Any]:
|
||||
raise AssertionError("Quick Connect must not provision the K1 over BLE")
|
||||
async def forbidden_provisioning_write(*_: object, **__: object) -> dict[str, Any]:
|
||||
raise AssertionError("Quick Connect must not send router credentials to the K1")
|
||||
|
||||
monkeypatch.setattr(facade_module, "associate_with_wifi_once", fake_associate)
|
||||
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_ble_write)
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"device_ap_activation_session",
|
||||
fake_activation_session,
|
||||
)
|
||||
monkeypatch.setattr(facade_module, "associate_with_wifi_profile_once", fake_associate)
|
||||
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provisioning_write)
|
||||
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
||||
|
||||
state = asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
ssid="XGR-TEST",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert activation_calls == ["k1-a"]
|
||||
assert not ble_session_open
|
||||
assert len(association_calls) == 1
|
||||
assert association_calls[0][0].name == "associate_wifi.swift"
|
||||
assert association_calls[0][1:] == ("XGR-TEST", PRIMARY_TEST_CREDENTIAL)
|
||||
assert association_calls[0][1] == facade_module.quick_connect_host_profile_id(
|
||||
"XGR-TEST-A"
|
||||
)
|
||||
assert association_calls[0][2] == "XGR-TEST-A"
|
||||
assert state["connection_mode"] == "quick-connect"
|
||||
assert state["k1_ip"] == "192.168.56.1"
|
||||
assert state["compatibility"]["attestation"]["topology"] == "device-ap"
|
||||
@@ -1909,7 +2106,12 @@ def test_quick_connect_associates_the_host_without_a_ble_write(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert PRIMARY_TEST_CREDENTIAL not in redacted_manifest
|
||||
assert "XGR-TEST" not in redacted_manifest
|
||||
assert "host_wifi_profile_id" in redacted_manifest
|
||||
assert '"host_wifi_profile_ready_before_device_write": true' in redacted_manifest
|
||||
assert '"credentials_resolved_by_plugin": true' in redacted_manifest
|
||||
assert "credential_provider_id" in redacted_manifest
|
||||
assert "device_ap_activation_profile_id" in redacted_manifest
|
||||
assert (quick_sessions[0] / "ap-activation.redacted.json").exists()
|
||||
assert service._camera_target_for_session( # noqa: SLF001
|
||||
state["device_session"]["device_session_id"]
|
||||
) == "192.168.56.1"
|
||||
@@ -1928,7 +2130,7 @@ def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
|
||||
service._devices = [{"device_id": "k1-a", "name": "XGR-TEST-A"}] # noqa: SLF001
|
||||
provisioning_calls: list[tuple[str, str, str]] = []
|
||||
|
||||
async def fake_provision(
|
||||
@@ -1952,7 +2154,7 @@ def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
|
||||
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"associate_with_wifi_once",
|
||||
"associate_with_wifi_profile_once",
|
||||
forbidden_host_association,
|
||||
)
|
||||
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
||||
@@ -1979,28 +2181,182 @@ def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
|
||||
)
|
||||
|
||||
|
||||
def test_quick_connect_missing_credential_provider_stops_before_ap_write(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._devices = [{"device_id": "k1-a", "name": "XGR-TEST-A"}] # noqa: SLF001
|
||||
service._selected_device_id = "previous-k1" # noqa: SLF001
|
||||
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
||||
service._connection_mode = "bridge" # noqa: SLF001
|
||||
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"ensure_wifi_profile_from_credential_source",
|
||||
lambda *_args, **_kwargs: {
|
||||
"schema_version": 1,
|
||||
"adapter": "macOS Keychain",
|
||||
"available": False,
|
||||
"profile_enrolled": False,
|
||||
"credential_source": None,
|
||||
},
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def forbidden_activation(
|
||||
*_args: object, **_kwargs: object
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
raise AssertionError("missing host credential must stop before the K1 AP write")
|
||||
yield {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"device_ap_activation_session",
|
||||
forbidden_activation,
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="credential-source-unavailable"):
|
||||
asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
state = service.state()
|
||||
assert state["selected_device_id"] == "previous-k1"
|
||||
assert state["k1_ip"] == "192.168.1.20"
|
||||
assert state["connection_mode"] == "bridge"
|
||||
assert not list(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
||||
operation = next(
|
||||
item for item in state["operations"] if item["action"] == "network.provision"
|
||||
)
|
||||
assert operation["status"] == "failed"
|
||||
assert operation["error"]["side_effect_status"] == "none"
|
||||
assert operation["error"]["safe_to_retry"] is True
|
||||
|
||||
|
||||
def test_quick_connect_does_not_start_host_wifi_without_ap_ready_flag(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._devices = [{"device_id": "k1-a", "name": "XGR-TEST-A"}] # noqa: SLF001
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"ensure_wifi_profile_from_credential_source",
|
||||
lambda *_args, **_kwargs: {
|
||||
"schema_version": 1,
|
||||
"adapter": "macOS Keychain",
|
||||
"available": True,
|
||||
"profile_enrolled": False,
|
||||
"credential_source": "exact-firmware-profile",
|
||||
},
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def not_ready_session(
|
||||
*_: object, **__: object
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
yield {
|
||||
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
||||
"started_at_utc": "2026-07-19T15:00:00Z",
|
||||
"completed_at_utc": "2026-07-19T15:00:15Z",
|
||||
"outcome": "no_status_change_before_timeout",
|
||||
"ready_observed": False,
|
||||
"write_performed": True,
|
||||
"write_mode": "with_response",
|
||||
}
|
||||
|
||||
def forbidden_association(*_: object, **__: object) -> dict[str, Any]:
|
||||
raise AssertionError("host Wi-Fi must wait for the canonical AP-ready flag")
|
||||
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"device_ap_activation_session",
|
||||
not_ready_session,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"associate_with_wifi_profile_once",
|
||||
forbidden_association,
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="не подтвердил готовность"):
|
||||
asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
quick_sessions = sorted(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
||||
assert len(quick_sessions) == 1
|
||||
assert (quick_sessions[0] / "ap-activation.redacted.json").exists()
|
||||
assert not (quick_sessions[0] / "manifest.redacted.json").exists()
|
||||
|
||||
|
||||
def test_failed_connection_change_revokes_the_previous_route(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
|
||||
service._devices = [{"device_id": "k1-a", "name": "XGR-TEST-A"}] # noqa: SLF001
|
||||
service._selected_device_id = "previous-k1" # noqa: SLF001
|
||||
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
||||
service._connection_mode = "bridge" # noqa: SLF001
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"ensure_wifi_profile_from_credential_source",
|
||||
lambda *_args, **_kwargs: {
|
||||
"schema_version": 1,
|
||||
"adapter": "macOS Keychain",
|
||||
"available": True,
|
||||
"profile_enrolled": False,
|
||||
"credential_source": "exact-firmware-profile",
|
||||
},
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def fake_activation_session(
|
||||
*_: object, **__: object
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
yield {
|
||||
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
||||
"started_at_utc": "2026-07-19T15:00:00Z",
|
||||
"completed_at_utc": "2026-07-19T15:00:01Z",
|
||||
"outcome": "ap_ready_observed",
|
||||
"ready_observed": True,
|
||||
"write_performed": True,
|
||||
"write_mode": "with_response",
|
||||
}
|
||||
|
||||
def failed_association(*_: object, **__: object) -> dict[str, Any]:
|
||||
raise RuntimeError("offline association fixture failed")
|
||||
raise facade_module.HostWifiProfileError(
|
||||
"network-not-found",
|
||||
scan_attempt_count=4,
|
||||
scan_elapsed_ms=15014,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(facade_module, "associate_with_wifi_once", failed_association)
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"device_ap_activation_session",
|
||||
fake_activation_session,
|
||||
)
|
||||
monkeypatch.setattr(facade_module, "associate_with_wifi_profile_once", failed_association)
|
||||
|
||||
with pytest.raises(RuntimeError, match="offline association fixture failed"):
|
||||
with pytest.raises(RuntimeError, match="network-not-found"):
|
||||
asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
ssid="XGR-OFFLINE",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
@@ -2012,6 +2368,15 @@ def test_failed_connection_change_revokes_the_previous_route(
|
||||
assert state["k1_ip"] is None
|
||||
assert state["connection_mode"] is None
|
||||
assert state["compatibility"]["attestation"] is None
|
||||
quick_sessions = sorted(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
||||
failure_evidence = json.loads(
|
||||
(quick_sessions[0] / "host-wifi-association.redacted.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
assert failure_evidence["reason_code"] == "network-not-found"
|
||||
assert failure_evidence["scan_attempt_count"] == 4
|
||||
assert failure_evidence["scan_elapsed_ms"] == 15014
|
||||
|
||||
|
||||
def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
|
||||
@@ -2027,7 +2392,7 @@ def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
|
||||
"completed_at_utc": "2026-07-18T15:19:26Z",
|
||||
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
||||
"outcome": "lan_address_observed",
|
||||
"observations": [{"status": {"ipv4": "192.168.68.51"}}],
|
||||
"observations": [{"status": {"ipv4": "10.255.254.51"}}],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from k1link.device_plugins.xgrids_k1.ble.ap_activation import (
|
||||
COMMAND_OFFSET,
|
||||
ENABLE_AP_COMMAND,
|
||||
FRAME_LENGTH,
|
||||
build_ap_activation_frame,
|
||||
is_ap_ready_status,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.ble.wifi_provisioning import WifiStatus
|
||||
|
||||
|
||||
def test_build_ap_activation_frame_matches_reviewed_lixelgo_layout() -> None:
|
||||
frame = build_ap_activation_frame()
|
||||
|
||||
assert len(frame) == FRAME_LENGTH == 100
|
||||
assert frame[COMMAND_OFFSET] == ENABLE_AP_COMMAND == 1
|
||||
assert frame[:COMMAND_OFFSET] == bytes(COMMAND_OFFSET)
|
||||
|
||||
|
||||
def _status(*, reserved: int) -> WifiStatus:
|
||||
return {
|
||||
"value_length": 52,
|
||||
"mode": "WIFI_AP",
|
||||
"ipv4": "192.168.56.1",
|
||||
"status_code": 1,
|
||||
"reserved": reserved,
|
||||
"trailer_hex": "",
|
||||
}
|
||||
|
||||
|
||||
def test_ap_control_mode_without_ready_flag_is_not_ready() -> None:
|
||||
assert not is_ap_ready_status(_status(reserved=0))
|
||||
|
||||
|
||||
def test_ap_ready_requires_the_reviewed_byte_51_flag() -> None:
|
||||
assert is_ap_ready_status(_status(reserved=1))
|
||||
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
|
||||
CalibratedProjectionError,
|
||||
Kb4ProjectionProfile,
|
||||
depth_colors,
|
||||
map_points_to_lidar,
|
||||
project_map_points_kb4,
|
||||
quaternion_xyzw_to_rotation_matrix,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.calibration_schema import (
|
||||
parse_k1_factory_calibration,
|
||||
)
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures" / "k1" / "calibration"
|
||||
|
||||
|
||||
def _identity_profile() -> Kb4ProjectionProfile:
|
||||
transform = np.eye(4, dtype=np.float64)
|
||||
transform.setflags(write=False)
|
||||
return Kb4ProjectionProfile(
|
||||
source_id="sensor.camera.right",
|
||||
calibration_slot="camera_1",
|
||||
width=800,
|
||||
height=600,
|
||||
intrinsic_fx_fy_cx_cy=(100.0, 100.0, 400.0, 300.0),
|
||||
distortion_kb4=(0.0, 0.0, 0.0, 0.0),
|
||||
t_camera_from_lidar=transform,
|
||||
)
|
||||
|
||||
|
||||
def test_pose_inverse_maps_world_points_back_into_lidar_frame() -> None:
|
||||
half = math.sqrt(0.5)
|
||||
points_lidar = map_points_to_lidar(
|
||||
[[10.0, 21.0, 30.0]],
|
||||
position_map_xyz=(10.0, 20.0, 30.0),
|
||||
orientation_map_from_lidar_xyzw=(0.0, 0.0, half, half),
|
||||
)
|
||||
np.testing.assert_allclose(points_lidar, [[1.0, 0.0, 0.0]], atol=1e-12)
|
||||
|
||||
|
||||
def test_quaternion_rotation_is_orthonormal_after_normalization() -> None:
|
||||
rotation = quaternion_xyzw_to_rotation_matrix((0.0, 0.0, 2.0, 2.0))
|
||||
assert rotation @ rotation.T == pytest.approx(np.eye(3), abs=1e-12)
|
||||
assert np.linalg.det(rotation) == pytest.approx(1.0, abs=1e-12)
|
||||
|
||||
|
||||
def test_kb4_projection_uses_theta_polynomial_and_rejects_behind_camera() -> None:
|
||||
projected = project_map_points_kb4(
|
||||
[[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 0.0, -1.0]],
|
||||
position_map_xyz=(0.0, 0.0, 0.0),
|
||||
orientation_map_from_lidar_xyzw=(0.0, 0.0, 0.0, 1.0),
|
||||
profile=_identity_profile(),
|
||||
)
|
||||
assert projected.source_point_count == 3
|
||||
assert projected.camera_front_point_count == 2
|
||||
assert projected.projected_point_count == 2
|
||||
assert projected.pixels_xy[0] == pytest.approx([400.0, 300.0])
|
||||
assert projected.pixels_xy[1] == pytest.approx(
|
||||
[400.0 + 100.0 * math.pi / 4.0, 300.0]
|
||||
)
|
||||
assert projected.source_indices.tolist() == [0, 1]
|
||||
|
||||
|
||||
def test_factory_profile_binds_right_main_camera_and_scales_intrinsics() -> None:
|
||||
calibration = parse_k1_factory_calibration(
|
||||
(FIXTURES / "camera.yaml").read_bytes(),
|
||||
(FIXTURES / "extrinsic_camera_lidar.yaml").read_bytes(),
|
||||
)
|
||||
profile = Kb4ProjectionProfile.from_factory_calibration(
|
||||
calibration,
|
||||
"sensor.camera.right",
|
||||
)
|
||||
camera = calibration.camera("camera_1")
|
||||
assert profile.calibration_slot == "camera_1"
|
||||
assert (profile.width, profile.height) == (800, 600)
|
||||
assert profile.intrinsic_fx_fy_cx_cy == pytest.approx(
|
||||
tuple(value * 0.2 for value in camera.intrinsic)
|
||||
)
|
||||
assert profile.t_camera_from_lidar == pytest.approx(
|
||||
np.asarray(calibration.t_camera_from_lidar("camera_1"))
|
||||
)
|
||||
|
||||
|
||||
def test_projection_rejects_unknown_camera_and_invalid_quaternion() -> None:
|
||||
calibration = parse_k1_factory_calibration(
|
||||
(FIXTURES / "camera.yaml").read_bytes(),
|
||||
(FIXTURES / "extrinsic_camera_lidar.yaml").read_bytes(),
|
||||
)
|
||||
with pytest.raises(CalibratedProjectionError, match="admitted K1 main camera"):
|
||||
Kb4ProjectionProfile.from_factory_calibration(calibration, "sensor.camera.unknown")
|
||||
with pytest.raises(CalibratedProjectionError, match="no usable norm"):
|
||||
quaternion_xyzw_to_rotation_matrix((0.0, 0.0, 0.0, 0.0))
|
||||
|
||||
|
||||
def test_depth_colors_are_bounded_and_repeatable() -> None:
|
||||
first = depth_colors([1.0, 2.0, 3.0])
|
||||
second = depth_colors([1.0, 2.0, 3.0])
|
||||
assert first.dtype == np.uint8
|
||||
assert first.shape == (3, 3)
|
||||
assert np.array_equal(first, second)
|
||||
assert first[0, 0] > first[0, 2]
|
||||
assert first[-1, 2] > first[-1, 0]
|
||||
@@ -0,0 +1,422 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import deque
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import pytest
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.calibration_snapshot import (
|
||||
seal_factory_calibration_snapshot,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
||||
ApplicationControlAuthority,
|
||||
LiveDeviceControlBinding,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.calibration_file import (
|
||||
CALIBRATION_FILE_READ_COMMAND,
|
||||
CALIBRATION_FILE_REQUEST_TOPIC,
|
||||
CALIBRATION_FILE_RESPONSE_TOPIC,
|
||||
FACTORY_CALIBRATION_PATHS,
|
||||
FACTORY_CAMERA_CALIBRATION_PATH,
|
||||
FACTORY_CAMERA_LIDAR_EXTRINSIC_PATH,
|
||||
CalibrationFileProtocolError,
|
||||
CalibrationFileRejected,
|
||||
build_factory_calibration_file_read,
|
||||
decode_factory_calibration_file_response,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.calibration_mqtt import (
|
||||
CALIBRATION_READ_SUBSCRIPTIONS,
|
||||
FactoryCalibrationReadResult,
|
||||
ReviewedCalibrationMqttReader,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import OPENAPI_SUCCESS
|
||||
from k1link.device_plugins.xgrids_k1.protocol.protobuf_wire import iter_fields
|
||||
|
||||
APPLICATION_KEY = "11111111-2222-3333-4444-555555555555"
|
||||
VENDOR_DEVICE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
DEVICE_SERIAL = "K1SERIAL01"
|
||||
PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
|
||||
CALIBRATION_FIXTURE_ROOT = Path(__file__).parent / "fixtures" / "k1" / "calibration"
|
||||
|
||||
|
||||
def _authority() -> ApplicationControlAuthority:
|
||||
return ApplicationControlAuthority(openapi_key=APPLICATION_KEY)
|
||||
|
||||
|
||||
def _binding(**changes: object) -> LiveDeviceControlBinding:
|
||||
values = {
|
||||
"vendor_device_id": VENDOR_DEVICE_ID,
|
||||
"device_serial": DEVICE_SERIAL,
|
||||
"software_version": "V3.0.2",
|
||||
"system_version": "V3.0.2",
|
||||
"device_model": "LixelKity K1",
|
||||
"device_type": "A4",
|
||||
"is_activated": True,
|
||||
}
|
||||
values.update(changes)
|
||||
return LiveDeviceControlBinding(**values) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _varint(value: int) -> bytes:
|
||||
encoded = bytearray()
|
||||
while value > 0x7F:
|
||||
encoded.append((value & 0x7F) | 0x80)
|
||||
value >>= 7
|
||||
encoded.append(value)
|
||||
return bytes(encoded)
|
||||
|
||||
|
||||
def _uint(number: int, value: int) -> bytes:
|
||||
return _varint(number << 3) + _varint(value)
|
||||
|
||||
|
||||
def _bytes(number: int, value: bytes) -> bytes:
|
||||
return _varint((number << 3) | 2) + _varint(len(value)) + value
|
||||
|
||||
|
||||
def _text(number: int, value: str) -> bytes:
|
||||
return _bytes(number, value.encode("utf-8"))
|
||||
|
||||
|
||||
def _header(session_id: str) -> bytes:
|
||||
return b"".join(
|
||||
(
|
||||
_text(4, VENDOR_DEVICE_ID),
|
||||
_text(5, session_id),
|
||||
_text(6, APPLICATION_KEY),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _device_info_response() -> bytes:
|
||||
base_info = b"".join(
|
||||
(
|
||||
_text(2, "V3.0.2"),
|
||||
_text(3, "V3.0.2"),
|
||||
_text(6, "LixelKity K1"),
|
||||
_text(7, DEVICE_SERIAL),
|
||||
_text(8, "A4"),
|
||||
)
|
||||
)
|
||||
working_status = _uint(1, 1)
|
||||
device_info = _bytes(2, base_info) + _bytes(7, working_status)
|
||||
return b"".join(
|
||||
(
|
||||
_bytes(1, _header(":DeviceInfoRequest")),
|
||||
_bytes(2, device_info),
|
||||
_bytes(15, _uint(1, OPENAPI_SUCCESS)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _calibration_response(
|
||||
path: str,
|
||||
content: bytes,
|
||||
*,
|
||||
command: int = CALIBRATION_FILE_READ_COMMAND,
|
||||
result_code: int = OPENAPI_SUCCESS,
|
||||
session_id: str | None = None,
|
||||
) -> bytes:
|
||||
return b"".join(
|
||||
(
|
||||
_bytes(
|
||||
1,
|
||||
_header(
|
||||
session_id or f"{VENDOR_DEVICE_ID}:CalibFileRequest"
|
||||
),
|
||||
),
|
||||
_uint(2, command),
|
||||
_text(3, path),
|
||||
_bytes(4, content),
|
||||
_bytes(15, _uint(1, result_code)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _content_for(path: str) -> bytes:
|
||||
if path == FACTORY_CAMERA_CALIBRATION_PATH:
|
||||
return (CALIBRATION_FIXTURE_ROOT / "camera.yaml").read_bytes()
|
||||
return (CALIBRATION_FIXTURE_ROOT / "extrinsic_camera_lidar.yaml").read_bytes()
|
||||
|
||||
|
||||
def test_factory_file_request_is_command_five_and_has_no_content_field() -> None:
|
||||
request = build_factory_calibration_file_read(
|
||||
_authority(),
|
||||
_binding(),
|
||||
FACTORY_CAMERA_CALIBRATION_PATH,
|
||||
)
|
||||
fields = {field.number: field.value for field in iter_fields(request.payload)}
|
||||
header = fields[1]
|
||||
assert isinstance(header, bytes)
|
||||
header_fields = {field.number: field.value for field in iter_fields(header)}
|
||||
|
||||
assert request.topic == CALIBRATION_FILE_REQUEST_TOPIC
|
||||
assert request.response_topic == CALIBRATION_FILE_RESPONSE_TOPIC
|
||||
assert request.mutates_device is False
|
||||
assert request.automatic_retry is False
|
||||
assert fields[2] == 5
|
||||
assert fields[3] == FACTORY_CAMERA_CALIBRATION_PATH.encode()
|
||||
assert 4 not in fields
|
||||
assert header_fields[4] == VENDOR_DEVICE_ID.encode()
|
||||
assert header_fields[5] == f"{VENDOR_DEVICE_ID}:CalibFileRequest".encode()
|
||||
assert header_fields[6] == APPLICATION_KEY.encode()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
(
|
||||
"/mnt/system/factory-data/config/../Lixel.yaml",
|
||||
"/mnt/system/factory-data/config/extrinsic_camera_motor.yaml",
|
||||
"/tmp/camera.yaml",
|
||||
"",
|
||||
),
|
||||
)
|
||||
def test_factory_file_request_rejects_every_path_outside_two_file_allowlist(
|
||||
path: str,
|
||||
) -> None:
|
||||
with pytest.raises(CalibrationFileProtocolError, match="two-file allowlist"):
|
||||
build_factory_calibration_file_read(_authority(), _binding(), path)
|
||||
|
||||
|
||||
def test_factory_file_request_rejects_a_nonreviewed_live_binding() -> None:
|
||||
with pytest.raises(CalibrationFileProtocolError, match="reviewed activated"):
|
||||
build_factory_calibration_file_read(
|
||||
_authority(),
|
||||
_binding(system_version="V3.0.3"),
|
||||
FACTORY_CAMERA_CALIBRATION_PATH,
|
||||
)
|
||||
|
||||
|
||||
def test_correlated_factory_file_response_returns_exact_utf8_bytes() -> None:
|
||||
request = build_factory_calibration_file_read(
|
||||
_authority(),
|
||||
_binding(),
|
||||
FACTORY_CAMERA_CALIBRATION_PATH,
|
||||
)
|
||||
content = _content_for(request.path)
|
||||
|
||||
result = decode_factory_calibration_file_response(
|
||||
_calibration_response(request.path, content),
|
||||
request,
|
||||
_authority(),
|
||||
_binding(),
|
||||
)
|
||||
|
||||
assert result.path == request.path
|
||||
assert result.content == content
|
||||
assert result.content_bytes == len(content)
|
||||
assert len(result.content_sha256) == 64
|
||||
assert result.result_code == OPENAPI_SUCCESS
|
||||
|
||||
|
||||
def test_factory_file_response_rejects_write_command_path_drift_and_duplicates() -> None:
|
||||
request = build_factory_calibration_file_read(
|
||||
_authority(),
|
||||
_binding(),
|
||||
FACTORY_CAMERA_CALIBRATION_PATH,
|
||||
)
|
||||
with pytest.raises(CalibrationFileProtocolError, match="not a file-read"):
|
||||
decode_factory_calibration_file_response(
|
||||
_calibration_response(request.path, b"yaml", command=6),
|
||||
request,
|
||||
_authority(),
|
||||
_binding(),
|
||||
)
|
||||
with pytest.raises(CalibrationFileProtocolError, match="path mismatch"):
|
||||
decode_factory_calibration_file_response(
|
||||
_calibration_response(FACTORY_CAMERA_LIDAR_EXTRINSIC_PATH, b"yaml"),
|
||||
request,
|
||||
_authority(),
|
||||
_binding(),
|
||||
)
|
||||
duplicate_path = _calibration_response(request.path, b"yaml") + _text(3, request.path)
|
||||
with pytest.raises(CalibrationFileProtocolError, match="duplicated"):
|
||||
decode_factory_calibration_file_response(
|
||||
duplicate_path,
|
||||
request,
|
||||
_authority(),
|
||||
_binding(),
|
||||
)
|
||||
|
||||
|
||||
def test_factory_file_response_rejects_device_error_and_non_utf8_content() -> None:
|
||||
request = build_factory_calibration_file_read(
|
||||
_authority(),
|
||||
_binding(),
|
||||
FACTORY_CAMERA_CALIBRATION_PATH,
|
||||
)
|
||||
with pytest.raises(CalibrationFileRejected) as rejected:
|
||||
decode_factory_calibration_file_response(
|
||||
_calibration_response(request.path, b"yaml", result_code=123),
|
||||
request,
|
||||
_authority(),
|
||||
_binding(),
|
||||
)
|
||||
assert rejected.value.result_code == 123
|
||||
with pytest.raises(CalibrationFileProtocolError, match="valid UTF-8"):
|
||||
decode_factory_calibration_file_response(
|
||||
_calibration_response(request.path, b"\xff"),
|
||||
request,
|
||||
_authority(),
|
||||
_binding(),
|
||||
)
|
||||
|
||||
|
||||
class FakeReasonCode:
|
||||
def __init__(self, *, failure: bool = False) -> None:
|
||||
self.is_failure = failure
|
||||
|
||||
|
||||
class FakeCalibrationClient:
|
||||
def __init__(self) -> None:
|
||||
self.on_connect: Any = None
|
||||
self.on_subscribe: Any = None
|
||||
self.on_publish: Any = None
|
||||
self.on_message: Any = None
|
||||
self.on_disconnect: Any = None
|
||||
self.connect_timeout = 0.0
|
||||
self.events: deque[tuple[str, object]] = deque()
|
||||
self.connect_calls: list[tuple[str, int, int]] = []
|
||||
self.subscribe_calls: list[list[tuple[str, int]]] = []
|
||||
self.publish_calls: list[tuple[str, bytes, int, bool]] = []
|
||||
self.unsubscribe_calls: list[list[str]] = []
|
||||
self.next_mid = 20
|
||||
|
||||
def connect(self, host: str, port: int, keepalive: int) -> mqtt.MQTTErrorCode:
|
||||
self.connect_calls.append((host, port, keepalive))
|
||||
self.events.append(("connect", FakeReasonCode()))
|
||||
return mqtt.MQTT_ERR_SUCCESS
|
||||
|
||||
def subscribe(self, topics: list[tuple[str, int]]) -> tuple[mqtt.MQTTErrorCode, int]:
|
||||
self.subscribe_calls.append(topics)
|
||||
self.events.append(("subscribe", 7))
|
||||
return mqtt.MQTT_ERR_SUCCESS, 7
|
||||
|
||||
def publish(
|
||||
self,
|
||||
topic: str,
|
||||
payload: bytes,
|
||||
qos: int,
|
||||
retain: bool,
|
||||
) -> SimpleNamespace:
|
||||
self.publish_calls.append((topic, payload, qos, retain))
|
||||
mid = self.next_mid
|
||||
self.next_mid += 1
|
||||
self.events.append(("publish", mid))
|
||||
if topic == "lixel/application/request/device_info":
|
||||
response_topic = "lixel/application/response/device_info"
|
||||
response_payload = _device_info_response()
|
||||
else:
|
||||
fields = {field.number: field.value for field in iter_fields(payload)}
|
||||
raw_path = fields[3]
|
||||
assert isinstance(raw_path, bytes)
|
||||
path = raw_path.decode()
|
||||
response_topic = CALIBRATION_FILE_RESPONSE_TOPIC
|
||||
response_payload = _calibration_response(path, _content_for(path))
|
||||
self.events.append(("message", (response_topic, response_payload)))
|
||||
return SimpleNamespace(rc=mqtt.MQTT_ERR_SUCCESS, mid=mid)
|
||||
|
||||
def loop(self, timeout: float) -> mqtt.MQTTErrorCode:
|
||||
del timeout
|
||||
if not self.events:
|
||||
return mqtt.MQTT_ERR_SUCCESS
|
||||
kind, value = self.events.popleft()
|
||||
if kind == "connect":
|
||||
self.on_connect(self, None, SimpleNamespace(), value, None)
|
||||
elif kind == "subscribe":
|
||||
self.on_subscribe(
|
||||
self,
|
||||
None,
|
||||
value,
|
||||
[FakeReasonCode() for _topic in CALIBRATION_READ_SUBSCRIPTIONS],
|
||||
None,
|
||||
)
|
||||
elif kind == "publish":
|
||||
self.on_publish(self, None, value, FakeReasonCode(), None)
|
||||
elif kind == "message":
|
||||
topic, payload = value
|
||||
self.on_message(self, None, SimpleNamespace(topic=topic, payload=payload))
|
||||
return mqtt.MQTT_ERR_SUCCESS
|
||||
|
||||
def unsubscribe(self, topics: list[str]) -> tuple[mqtt.MQTTErrorCode, int]:
|
||||
self.unsubscribe_calls.append(topics)
|
||||
return mqtt.MQTT_ERR_SUCCESS, 50
|
||||
|
||||
def disconnect(self) -> mqtt.MQTTErrorCode:
|
||||
return mqtt.MQTT_ERR_SUCCESS
|
||||
|
||||
|
||||
def test_reviewed_mqtt_reader_performs_one_identity_read_then_two_exact_file_reads() -> None:
|
||||
client = FakeCalibrationClient()
|
||||
reader = ReviewedCalibrationMqttReader(
|
||||
"10.255.254.54",
|
||||
client_factory=lambda: client, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
result = reader.read_factory_calibration(_authority())
|
||||
|
||||
assert result.binding == _binding()
|
||||
assert [item.path for item in result.files] == list(FACTORY_CALIBRATION_PATHS)
|
||||
assert client.subscribe_calls == [list(CALIBRATION_READ_SUBSCRIPTIONS)]
|
||||
assert [topic for topic, _payload, _qos, _retain in client.publish_calls] == [
|
||||
"lixel/application/request/device_info",
|
||||
CALIBRATION_FILE_REQUEST_TOPIC,
|
||||
CALIBRATION_FILE_REQUEST_TOPIC,
|
||||
]
|
||||
assert all(qos == 2 and not retain for _topic, _payload, qos, retain in client.publish_calls)
|
||||
assert result.transport["publish_attempts"] == 3
|
||||
assert result.transport["correlated_responses"] == 3
|
||||
assert result.transport["write_command_available"] is False
|
||||
|
||||
|
||||
def test_private_snapshot_is_new_only_and_hashes_both_exact_artifacts(tmp_path: Path) -> None:
|
||||
files = tuple(
|
||||
decode_factory_calibration_file_response(
|
||||
_calibration_response(path, _content_for(path)),
|
||||
build_factory_calibration_file_read(_authority(), _binding(), path),
|
||||
_authority(),
|
||||
_binding(),
|
||||
)
|
||||
for path in FACTORY_CALIBRATION_PATHS
|
||||
)
|
||||
result = FactoryCalibrationReadResult(
|
||||
binding=_binding(),
|
||||
files=(files[0], files[1]),
|
||||
transport={"automatic_retry": False, "publish_attempts": 3},
|
||||
)
|
||||
|
||||
snapshot = seal_factory_calibration_snapshot(
|
||||
result,
|
||||
evidence_root=tmp_path,
|
||||
compatibility_profile_id=PROFILE_ID,
|
||||
captured_at=datetime(2026, 7, 20, 10, 30, tzinfo=UTC),
|
||||
)
|
||||
|
||||
assert snapshot["status"] == "available"
|
||||
internal = snapshot["device_internal_calibration"]
|
||||
assert isinstance(internal, dict)
|
||||
snapshot_path = Path(str(internal["private_snapshot_path"]))
|
||||
assert snapshot_path.is_dir()
|
||||
assert (snapshot_path / "camera.yaml").read_bytes() == _content_for(
|
||||
FACTORY_CAMERA_CALIBRATION_PATH
|
||||
)
|
||||
assert (snapshot_path / "extrinsic_camera_lidar.yaml").read_bytes() == _content_for(
|
||||
FACTORY_CAMERA_LIDAR_EXTRINSIC_PATH
|
||||
)
|
||||
manifest = json.loads((snapshot_path / "manifest.json").read_text(encoding="utf-8"))
|
||||
assert manifest["source"]["request_command"] == 5
|
||||
assert manifest["source"]["write_command_available"] is False
|
||||
assert manifest["normalized_calibration"]["transform_notation"] == (
|
||||
"T_destination_from_source"
|
||||
)
|
||||
assert [item["source_path"] for item in manifest["artifacts"]] == list(
|
||||
FACTORY_CALIBRATION_PATHS
|
||||
)
|
||||
assert snapshot_path.stat().st_mode & 0o777 == 0o700
|
||||
assert all(path.stat().st_mode & 0o777 == 0o600 for path in snapshot_path.iterdir())
|
||||
assert internal["camera_stream_mapping"]["status"] == "firmware-profile-verified"
|
||||
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.calibration_schema import (
|
||||
FactoryCalibrationSchemaError,
|
||||
parse_k1_factory_calibration,
|
||||
)
|
||||
|
||||
FIXTURE_ROOT = Path(__file__).parent / "fixtures" / "k1" / "calibration"
|
||||
|
||||
|
||||
def _documents() -> tuple[bytes, bytes]:
|
||||
return (
|
||||
(FIXTURE_ROOT / "camera.yaml").read_bytes(),
|
||||
(FIXTURE_ROOT / "extrinsic_camera_lidar.yaml").read_bytes(),
|
||||
)
|
||||
|
||||
|
||||
def test_factory_calibration_normalizes_main_camera_mapping_and_transforms() -> None:
|
||||
calibration = parse_k1_factory_calibration(*_documents())
|
||||
profile = calibration.normalized_profile()
|
||||
streams = profile["stream_bindings"]
|
||||
assert isinstance(streams, dict)
|
||||
|
||||
left = streams["sensor.camera.left"]
|
||||
right = streams["sensor.camera.right"]
|
||||
assert isinstance(left, dict)
|
||||
assert isinstance(right, dict)
|
||||
assert left["calibration_slot"] == "camera_0"
|
||||
assert right["calibration_slot"] == "camera_1"
|
||||
assert left["rtsp_path"] == "/live/chn_left_main"
|
||||
assert right["rtsp_path"] == "/live/chn_right_main"
|
||||
assert left["native_resolution"] == [4000, 3000]
|
||||
assert left["admitted_resolution"] == [800, 600]
|
||||
assert left["admitted_intrinsic_fx_fy_cx_cy"] == pytest.approx(
|
||||
[193.6, 193.6, 400.0, 300.0]
|
||||
)
|
||||
assert right["admitted_intrinsic_fx_fy_cx_cy"] == pytest.approx(
|
||||
[194.0, 194.2, 396.0, 302.0]
|
||||
)
|
||||
assert profile["transform_notation"] == "T_destination_from_source"
|
||||
|
||||
right_transform = calibration.t_camera_from_lidar("camera_1")
|
||||
assert right_transform[0] == pytest.approx((-1.0, 0.0, 0.0, 0.007))
|
||||
assert right_transform[1] == pytest.approx((0.0, 0.0, -1.0, -0.0948))
|
||||
assert right_transform[2] == pytest.approx((0.0, -1.0, 0.0, -0.0328))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mutation", "message"),
|
||||
[
|
||||
(
|
||||
lambda data: data.replace(
|
||||
b"calibrated: true\n",
|
||||
b"calibrated: true\ncalibrated: true\n",
|
||||
1,
|
||||
),
|
||||
"safe valid YAML",
|
||||
),
|
||||
(
|
||||
lambda data: data.replace(
|
||||
b"camera_pose: [1, 0, 0, 0,",
|
||||
b"camera_pose: &pose [1, 0, 0, 0,",
|
||||
1,
|
||||
),
|
||||
"anchors",
|
||||
),
|
||||
(
|
||||
lambda data: data.replace(b"intrinsic: [968,", b"intrinsic: [.nan,", 1),
|
||||
"finite",
|
||||
),
|
||||
(
|
||||
lambda data: data.replace(b"image_width: 4000", b"image_width: 3999", 1),
|
||||
"resolution",
|
||||
),
|
||||
(
|
||||
lambda data: data.replace(
|
||||
b"camera_model: kb4",
|
||||
b"camera_model: kb4\n unexpected: true",
|
||||
1,
|
||||
),
|
||||
"unexpected key set",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_factory_calibration_rejects_unsafe_or_nonprofile_camera_yaml(
|
||||
mutation: object,
|
||||
message: str,
|
||||
) -> None:
|
||||
camera, extrinsic = _documents()
|
||||
mutate = mutation
|
||||
assert callable(mutate)
|
||||
with pytest.raises(FactoryCalibrationSchemaError, match=message):
|
||||
parse_k1_factory_calibration(mutate(camera), extrinsic)
|
||||
|
||||
|
||||
def test_factory_calibration_rejects_version_or_rigid_transform_mismatch() -> None:
|
||||
camera, extrinsic = _documents()
|
||||
mismatched = extrinsic.replace(b"V2.2.0_alpha", b"V2.2.1_alpha")
|
||||
with pytest.raises(FactoryCalibrationSchemaError, match="versions do not match"):
|
||||
parse_k1_factory_calibration(camera, mismatched)
|
||||
|
||||
invalid_rotation = extrinsic.replace(
|
||||
b"transform: [1, 0, 0,",
|
||||
b"transform: [2, 0, 0,",
|
||||
)
|
||||
with pytest.raises(FactoryCalibrationSchemaError, match="not orthonormal"):
|
||||
parse_k1_factory_calibration(camera, invalid_rotation)
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
import k1link.device_plugins.xgrids_k1.camera as camera_module
|
||||
from k1link.device_plugins.xgrids_k1.camera import (
|
||||
CAMERA_MEDIA_TYPE,
|
||||
CommittedCameraSegment,
|
||||
XgridsK1CameraGateway,
|
||||
_build_ffmpeg_argv,
|
||||
_read_mp4_box,
|
||||
@@ -169,6 +170,45 @@ def test_camera_selection_is_exclusive_and_hides_device_transport(
|
||||
gateway.close()
|
||||
|
||||
|
||||
def test_camera_derived_observer_runs_only_after_durable_archive_commit(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path)))
|
||||
session = tmp_path / "session"
|
||||
session.mkdir()
|
||||
observed: list[CommittedCameraSegment] = []
|
||||
|
||||
def observe(segment: CommittedCameraSegment) -> None:
|
||||
archive = (
|
||||
session
|
||||
/ "media"
|
||||
/ segment.source_id
|
||||
/ f"epoch-{segment.generation}"
|
||||
)
|
||||
committed = (
|
||||
archive / "init.mp4"
|
||||
if segment.kind == "init"
|
||||
else archive / "segments" / f"{segment.sequence}.m4s"
|
||||
)
|
||||
assert committed.read_bytes() == segment.payload
|
||||
observed.append(segment)
|
||||
|
||||
gateway = XgridsK1CameraGateway(
|
||||
tmp_path,
|
||||
XGRIDS_K1_PLUGIN_ID,
|
||||
committed_segment_observer=observe,
|
||||
)
|
||||
try:
|
||||
gateway.start_recording(session)
|
||||
gateway.select("sensor.camera.right", "192.168.8.52")
|
||||
_wait_until(lambda: len(observed) == 2)
|
||||
assert [segment.kind for segment in observed] == ["init", "media"]
|
||||
assert gateway.snapshot()["derived_observer_errors"] == 0
|
||||
finally:
|
||||
gateway.close()
|
||||
|
||||
|
||||
def test_camera_ffmpeg_command_is_allowlisted_copy_remux() -> None:
|
||||
argv = _build_ffmpeg_argv(
|
||||
Path("/trusted/ffmpeg"),
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import k1link.device_plugins.xgrids_k1.firmware_credential as firmware_credential
|
||||
|
||||
FIXTURE_SECRET = b"fixture-only-network-secret"
|
||||
|
||||
|
||||
def _tar_gz_member(name: str, payload: bytes) -> bytes:
|
||||
output = io.BytesIO()
|
||||
with tarfile.open(fileobj=output, mode="w:gz") as archive:
|
||||
member = tarfile.TarInfo(name)
|
||||
member.size = len(payload)
|
||||
archive.addfile(member, io.BytesIO(payload))
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def test_exact_official_archive_extracts_one_bounded_declaration(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
prefix = b"synthetic-rk-prefix"
|
||||
apps = (
|
||||
b"\x00" * 17
|
||||
+ firmware_credential._AP_PSK_DECLARATION
|
||||
+ FIXTURE_SECRET
|
||||
+ b"\n"
|
||||
+ b"\x00" * 31
|
||||
)
|
||||
image = prefix + apps
|
||||
upgrade = _tar_gz_member(firmware_credential.K1_FW302_RK_IMAGE_MEMBER, image)
|
||||
outer = _tar_gz_member(firmware_credential.K1_FW302_OUTER_MEMBER, upgrade)
|
||||
archive_path = tmp_path / "official-fw.tar"
|
||||
archive_path.write_bytes(outer)
|
||||
|
||||
monkeypatch.setattr(firmware_credential, "K1_FW302_APPS_OFFSET", len(prefix))
|
||||
monkeypatch.setattr(firmware_credential, "K1_FW302_APPS_SIZE", len(apps))
|
||||
monkeypatch.setattr(
|
||||
firmware_credential,
|
||||
"_sha256",
|
||||
lambda _path: firmware_credential.K1_FW302_OFFICIAL_ARCHIVE_SHA256,
|
||||
)
|
||||
|
||||
secret, digest = firmware_credential.extract_k1_fw302_ap_credential(archive_path)
|
||||
try:
|
||||
assert secret.reveal_ascii() == FIXTURE_SECRET.decode("ascii")
|
||||
assert FIXTURE_SECRET.decode("ascii") not in repr(secret)
|
||||
assert digest == firmware_credential.K1_FW302_OFFICIAL_ARCHIVE_SHA256
|
||||
finally:
|
||||
secret.zeroize()
|
||||
|
||||
|
||||
def test_archive_hash_mismatch_stops_before_unpack(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
archive_path = tmp_path / "wrong-fw.tar"
|
||||
archive_path.write_bytes(b"not-reviewed")
|
||||
monkeypatch.setattr(firmware_credential, "_sha256", lambda _path: "0" * 64)
|
||||
|
||||
with pytest.raises(
|
||||
firmware_credential.FirmwareCredentialError,
|
||||
match="SHA-256",
|
||||
):
|
||||
firmware_credential.extract_k1_fw302_ap_credential(archive_path)
|
||||
|
||||
|
||||
def test_duplicate_declarations_fail_closed_without_exposing_values() -> None:
|
||||
payload = b"\n".join(
|
||||
[
|
||||
firmware_credential._AP_PSK_DECLARATION + FIXTURE_SECRET,
|
||||
firmware_credential._AP_PSK_DECLARATION + b"second-fixture-secret",
|
||||
]
|
||||
) + b"\n"
|
||||
|
||||
with pytest.raises(
|
||||
firmware_credential.FirmwareCredentialError,
|
||||
match="not unique",
|
||||
) as raised:
|
||||
firmware_credential._credential_from_chunks(iter([payload]))
|
||||
|
||||
assert FIXTURE_SECRET.decode("ascii") not in str(raised.value)
|
||||
+254
-31
@@ -6,9 +6,11 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.device_plugins.xgrids_k1 import macos_wifi
|
||||
from k1link.host_network import wifi
|
||||
|
||||
TEST_PASSWORD = "fixture-only-network-secret"
|
||||
TEST_PROFILE_ID = "fixture.quick-connect.v1"
|
||||
TEST_CREDENTIAL_SOURCE_ID = "fixture.firmware-provider.v1"
|
||||
|
||||
|
||||
def _helper(tmp_path: Path) -> Path:
|
||||
@@ -17,35 +19,33 @@ def _helper(tmp_path: Path) -> Path:
|
||||
return helper
|
||||
|
||||
|
||||
def test_association_passes_secret_only_through_stdin(
|
||||
def test_association_exposes_only_profile_id_and_expected_ssid_to_platform_helper(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(macos_wifi.sys, "platform", "darwin")
|
||||
monkeypatch.setattr(wifi.sys, "platform", "darwin")
|
||||
calls: list[dict[str, object]] = []
|
||||
live_inputs: list[bytearray] = []
|
||||
|
||||
def fake_runner(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]:
|
||||
assert isinstance(kwargs["input"], bytearray)
|
||||
live_inputs.append(kwargs["input"])
|
||||
calls.append(
|
||||
{
|
||||
"argv": argv,
|
||||
**kwargs,
|
||||
"input": bytes(kwargs["input"]),
|
||||
}
|
||||
)
|
||||
calls.append({"argv": argv, **kwargs, "input": bytes(kwargs["input"])})
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
stdout=b'{"ok":true,"already_associated":false}',
|
||||
stdout=(
|
||||
b'{"ok":true,"adapter":"CoreWLAN","already_associated":false,'
|
||||
b'"profile_enrolled":true,"scan_attempt_count":3,'
|
||||
b'"scan_elapsed_ms":1840,"credential_source":"system-wifi-keychain"}'
|
||||
),
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
result = macos_wifi.associate_with_wifi_once(
|
||||
result = wifi.associate_with_wifi_profile_once(
|
||||
_helper(tmp_path),
|
||||
TEST_PROFILE_ID,
|
||||
"XGR-OFFLINE",
|
||||
TEST_PASSWORD,
|
||||
runner=fake_runner,
|
||||
)
|
||||
|
||||
@@ -54,55 +54,278 @@ def test_association_passes_secret_only_through_stdin(
|
||||
"adapter": "CoreWLAN",
|
||||
"outcome": "associated",
|
||||
"already_associated": False,
|
||||
"profile_enrolled": True,
|
||||
"scan_attempt_count": 3,
|
||||
"scan_elapsed_ms": 1840,
|
||||
"credential_source": "system-wifi-keychain",
|
||||
}
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call["argv"][:2] == ["/usr/bin/xcrun", "swift"]
|
||||
assert TEST_PASSWORD not in " ".join(call["argv"])
|
||||
request = json.loads(bytes(call["input"]).decode("utf-8"))
|
||||
assert request == {"ssid": "XGR-OFFLINE", "password": TEST_PASSWORD}
|
||||
assert request == {
|
||||
"action": "associate",
|
||||
"profile_id": TEST_PROFILE_ID,
|
||||
"ssid": "XGR-OFFLINE",
|
||||
"scan_timeout_seconds": 15.0,
|
||||
}
|
||||
assert call["check"] is False
|
||||
assert call["timeout"] == 45.0
|
||||
assert call["timeout"] == 180.0
|
||||
assert live_inputs and not any(live_inputs[0])
|
||||
|
||||
|
||||
def test_profile_store_passes_secret_only_through_stdin(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(wifi.sys, "platform", "darwin")
|
||||
captured: dict[str, object] = {}
|
||||
live_inputs: list[bytearray] = []
|
||||
|
||||
def fake_runner(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]:
|
||||
assert isinstance(kwargs["input"], bytearray)
|
||||
live_inputs.append(kwargs["input"])
|
||||
captured.update({"argv": argv, **kwargs, "input": bytes(kwargs["input"])})
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
stdout=b'{"ok":true,"adapter":"macOS Keychain","stored":true}',
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
result = wifi.store_wifi_profile_once(
|
||||
_helper(tmp_path),
|
||||
TEST_PROFILE_ID,
|
||||
"XGR-OFFLINE",
|
||||
TEST_PASSWORD,
|
||||
runner=fake_runner,
|
||||
)
|
||||
|
||||
assert result["outcome"] == "stored"
|
||||
assert TEST_PASSWORD not in " ".join(captured["argv"])
|
||||
request = json.loads(bytes(captured["input"]).decode("utf-8"))
|
||||
assert request == {
|
||||
"action": "store-profile",
|
||||
"profile_id": TEST_PROFILE_ID,
|
||||
"ssid": "XGR-OFFLINE",
|
||||
"password": TEST_PASSWORD,
|
||||
}
|
||||
assert live_inputs and not any(live_inputs[0])
|
||||
|
||||
|
||||
def test_firmware_material_store_passes_secret_only_through_stdin(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(wifi.sys, "platform", "darwin")
|
||||
captured: dict[str, object] = {}
|
||||
live_inputs: list[bytearray] = []
|
||||
|
||||
def fake_runner(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]:
|
||||
assert isinstance(kwargs["input"], bytearray)
|
||||
live_inputs.append(kwargs["input"])
|
||||
captured.update({"argv": argv, **kwargs, "input": bytes(kwargs["input"])})
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
stdout=b'{"ok":true,"adapter":"macOS Keychain","stored":true}',
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
result = wifi.store_wifi_credential_material(
|
||||
_helper(tmp_path),
|
||||
TEST_CREDENTIAL_SOURCE_ID,
|
||||
TEST_PASSWORD,
|
||||
runner=fake_runner,
|
||||
)
|
||||
|
||||
assert result["outcome"] == "stored"
|
||||
assert TEST_PASSWORD not in " ".join(captured["argv"])
|
||||
request = json.loads(bytes(captured["input"]).decode("utf-8"))
|
||||
assert request == {
|
||||
"action": "store-credential-material",
|
||||
"profile_id": TEST_CREDENTIAL_SOURCE_ID,
|
||||
"password": TEST_PASSWORD,
|
||||
}
|
||||
assert live_inputs and not any(live_inputs[0])
|
||||
|
||||
|
||||
def test_profile_is_materialized_from_opaque_firmware_source(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(wifi.sys, "platform", "darwin")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_runner(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]:
|
||||
captured.update({"argv": argv, **kwargs, "input": bytes(kwargs["input"])})
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
stdout=(
|
||||
b'{"ok":true,"adapter":"macOS Keychain","profile_available":true,'
|
||||
b'"profile_enrolled":true,"credential_source":"exact-firmware-profile"}'
|
||||
),
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
result = wifi.ensure_wifi_profile_from_credential_source(
|
||||
_helper(tmp_path),
|
||||
TEST_PROFILE_ID,
|
||||
"XGR-OFFLINE",
|
||||
TEST_CREDENTIAL_SOURCE_ID,
|
||||
runner=fake_runner,
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"schema_version": 1,
|
||||
"adapter": "macOS Keychain",
|
||||
"available": True,
|
||||
"profile_enrolled": True,
|
||||
"credential_source": "exact-firmware-profile",
|
||||
}
|
||||
request = json.loads(bytes(captured["input"]).decode("utf-8"))
|
||||
assert request == {
|
||||
"action": "ensure-profile",
|
||||
"profile_id": TEST_PROFILE_ID,
|
||||
"ssid": "XGR-OFFLINE",
|
||||
"credential_source_id": TEST_CREDENTIAL_SOURCE_ID,
|
||||
}
|
||||
assert TEST_PASSWORD not in bytes(captured["input"]).decode("utf-8")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("available", [True, False])
|
||||
def test_profile_preflight_checks_only_the_expected_keychain_item(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
available: bool,
|
||||
) -> None:
|
||||
monkeypatch.setattr(wifi.sys, "platform", "darwin")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_runner(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]:
|
||||
captured.update({"argv": argv, **kwargs, "input": bytes(kwargs["input"])})
|
||||
encoded_available = b"true" if available else b"false"
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
stdout=(
|
||||
b'{"ok":true,"adapter":"macOS Keychain","profile_available":'
|
||||
+ encoded_available
|
||||
+ b"}"
|
||||
),
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
result = wifi.check_wifi_profile(
|
||||
_helper(tmp_path),
|
||||
TEST_PROFILE_ID,
|
||||
"XGR-OFFLINE",
|
||||
runner=fake_runner,
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"schema_version": 1,
|
||||
"adapter": "macOS Keychain",
|
||||
"available": available,
|
||||
}
|
||||
request = json.loads(bytes(captured["input"]).decode("utf-8"))
|
||||
assert request == {
|
||||
"action": "check-profile",
|
||||
"profile_id": TEST_PROFILE_ID,
|
||||
"ssid": "XGR-OFFLINE",
|
||||
}
|
||||
|
||||
|
||||
def test_association_reports_only_sanitized_helper_reason(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(macos_wifi.sys, "platform", "darwin")
|
||||
monkeypatch.setattr(wifi.sys, "platform", "darwin")
|
||||
|
||||
def fake_runner(argv: list[str], **_: object) -> subprocess.CompletedProcess[bytes]:
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
1,
|
||||
stdout=b'{"ok":false,"reason_code":"network-not-found"}',
|
||||
stdout=(
|
||||
b'{"ok":false,"reason_code":"network-not-found",'
|
||||
b'"scan_attempt_count":4,"scan_elapsed_ms":15021}'
|
||||
),
|
||||
stderr=f"private diagnostic {TEST_PASSWORD}".encode(),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
macos_wifi.HostWifiAssociationError,
|
||||
match="network-not-found",
|
||||
) as raised:
|
||||
macos_wifi.associate_with_wifi_once(
|
||||
with pytest.raises(wifi.HostWifiProfileError, match="network-not-found") as raised:
|
||||
wifi.associate_with_wifi_profile_once(
|
||||
_helper(tmp_path),
|
||||
TEST_PROFILE_ID,
|
||||
"XGR-OFFLINE",
|
||||
TEST_PASSWORD,
|
||||
runner=fake_runner,
|
||||
)
|
||||
|
||||
assert TEST_PASSWORD not in str(raised.value)
|
||||
assert raised.value.scan_attempt_count == 4
|
||||
assert raised.value.scan_elapsed_ms == 15021
|
||||
|
||||
|
||||
def test_association_is_macos_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(macos_wifi.sys, "platform", "linux")
|
||||
def test_association_reports_operator_timeout_separately_from_missing_helper(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(wifi.sys, "platform", "darwin")
|
||||
|
||||
def timed_out_runner(
|
||||
argv: list[str], **_: object
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
raise subprocess.TimeoutExpired(argv, timeout=180.0)
|
||||
|
||||
with pytest.raises(
|
||||
macos_wifi.HostWifiAssociationError,
|
||||
match="unsupported-platform",
|
||||
):
|
||||
macos_wifi.associate_with_wifi_once(
|
||||
wifi.HostWifiProfileError,
|
||||
match="host-wifi-operation-timeout",
|
||||
) as raised:
|
||||
wifi.associate_with_wifi_profile_once(
|
||||
_helper(tmp_path),
|
||||
TEST_PROFILE_ID,
|
||||
"XGR-OFFLINE",
|
||||
runner=timed_out_runner,
|
||||
)
|
||||
|
||||
assert raised.value.reason_code == "host-wifi-operation-timeout"
|
||||
|
||||
|
||||
def test_association_reports_an_unavailable_helper_separately_from_timeout(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(wifi.sys, "platform", "darwin")
|
||||
|
||||
def unavailable_runner(
|
||||
_: list[str], **__: object
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
raise OSError("offline fixture")
|
||||
|
||||
with pytest.raises(
|
||||
wifi.HostWifiProfileError,
|
||||
match="host-wifi-helper-unavailable",
|
||||
) as raised:
|
||||
wifi.associate_with_wifi_profile_once(
|
||||
_helper(tmp_path),
|
||||
TEST_PROFILE_ID,
|
||||
"XGR-OFFLINE",
|
||||
runner=unavailable_runner,
|
||||
)
|
||||
|
||||
assert raised.value.reason_code == "host-wifi-helper-unavailable"
|
||||
|
||||
|
||||
def test_association_rejects_an_uninstalled_platform(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(wifi.sys, "platform", "linux")
|
||||
|
||||
with pytest.raises(wifi.HostWifiProfileError, match="unsupported-platform"):
|
||||
wifi.associate_with_wifi_profile_once(
|
||||
_helper(tmp_path),
|
||||
TEST_PROFILE_ID,
|
||||
"XGR-OFFLINE",
|
||||
TEST_PASSWORD,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user