feat(lab): stabilize autonomous TGS playback
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from k1link.laboratory.m49_physical_safety_playback import (
|
||||
CHUNK_HEADER,
|
||||
CHUNK_MAGIC,
|
||||
read_m49_physical_safety_playback,
|
||||
seal_m49_physical_safety_playback,
|
||||
)
|
||||
from k1link.web.m49_physical_safety_playback_api import (
|
||||
build_m49_physical_safety_playback_router,
|
||||
)
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _sealed_source(root: Path, *, frame_count: int = 65, cell_count: int = 8) -> Path:
|
||||
root.mkdir()
|
||||
centers = np.column_stack(
|
||||
(
|
||||
np.arange(cell_count, dtype=np.float32) * np.float32(0.15),
|
||||
np.zeros(cell_count, dtype=np.float32),
|
||||
)
|
||||
).astype(np.float32)
|
||||
states = np.ones((frame_count, cell_count), dtype=np.uint8)
|
||||
z_bounds = np.zeros((frame_count, cell_count, 2), dtype=np.float32)
|
||||
z_bounds[..., 1] = np.float32(0.12)
|
||||
missing_sequence = 17
|
||||
states[missing_sequence] = 0
|
||||
z_bounds[missing_sequence] = np.nan
|
||||
np.save(root / "costmap-cell-centers-xy-m.npy", centers, allow_pickle=False)
|
||||
np.save(root / "costmap-states.npy", states, allow_pickle=False)
|
||||
np.save(root / "costmap-z-bounds-m.npy", z_bounds, allow_pickle=False)
|
||||
frames = [
|
||||
{
|
||||
"source_frame_index": sequence,
|
||||
"session_seconds": sequence / 10,
|
||||
"sample_available": sequence != missing_sequence,
|
||||
"eligible_point_count": 0 if sequence == missing_sequence else cell_count,
|
||||
"ground_point_count": 0 if sequence == missing_sequence else cell_count,
|
||||
"nonground_point_count": 0,
|
||||
"rejected_point_count": 0,
|
||||
"occupied_cell_count": 0,
|
||||
}
|
||||
for sequence in range(frame_count)
|
||||
]
|
||||
(root / "frames.ndjson").write_text(
|
||||
"".join(json.dumps(frame, sort_keys=True) + "\n" for frame in frames),
|
||||
encoding="utf-8",
|
||||
)
|
||||
source_identity = {"schema_version": "missioncore.test-sealed-source/v1"}
|
||||
source_identity_sha256 = hashlib.sha256(
|
||||
json.dumps(source_identity, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()
|
||||
result_id = "m49-tgs-full-shadow-" + source_identity_sha256
|
||||
report = {
|
||||
"schema_version": "missioncore.m49-tgs-full-shadow-report/v1",
|
||||
"result_id": result_id,
|
||||
"configuration": {
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"cell_size_m": 0.15,
|
||||
"radius_m": 12.0,
|
||||
},
|
||||
"visual_review": {
|
||||
"state_codes": {
|
||||
"UNOBSERVED": 0,
|
||||
"GROUND_SUPPORT": 1,
|
||||
"NONGROUND_OCCUPIED": 2,
|
||||
"UNKNOWN_REJECTED": 3,
|
||||
}
|
||||
},
|
||||
}
|
||||
(root / "report.json").write_text(json.dumps(report), encoding="utf-8")
|
||||
artifacts = []
|
||||
for name in (
|
||||
"costmap-cell-centers-xy-m.npy",
|
||||
"costmap-states.npy",
|
||||
"costmap-z-bounds-m.npy",
|
||||
"frames.ndjson",
|
||||
"report.json",
|
||||
):
|
||||
path = root / name
|
||||
artifacts.append(
|
||||
{
|
||||
"path": name,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
)
|
||||
(root / "manifest.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "missioncore.m49-tgs-full-shadow-lab/v1",
|
||||
"result_id": result_id,
|
||||
"identity": source_identity,
|
||||
"identity_sha256": source_identity_sha256,
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return root
|
||||
|
||||
|
||||
def test_local_builder_seals_bounded_chunks_without_worker_network(
|
||||
monkeypatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source = _sealed_source(tmp_path / "source")
|
||||
|
||||
def forbid_network(*_args: object, **_kwargs: object) -> None:
|
||||
raise AssertionError("local LAB builder attempted network access")
|
||||
|
||||
monkeypatch.setattr(socket, "create_connection", forbid_network)
|
||||
sealed = seal_m49_physical_safety_playback(
|
||||
source_root=source,
|
||||
destination_root=tmp_path / "results",
|
||||
profile_path=(
|
||||
REPOSITORY_ROOT / "config" / "perception" / "m49-physical-safety-shadow-v0.json"
|
||||
),
|
||||
created_at_utc="2026-08-27T12:00:00Z",
|
||||
)
|
||||
|
||||
assert sealed.manifest["execution"] == {
|
||||
"execution_class": "local-sequential-offline",
|
||||
"worker_role": "realtime-only",
|
||||
"worker_runtime_dependency": False,
|
||||
"worker_requests_required": 0,
|
||||
}
|
||||
playback = sealed.manifest["playback"]
|
||||
assert playback["cell_size_m"] == 0.15
|
||||
assert playback["chunk_frame_count"] == 32
|
||||
assert playback["startup_prebuffer_chunk_count"] == 2
|
||||
assert playback["resident_chunk_count_max"] == 3
|
||||
assert [(chunk["start"], chunk["count"]) for chunk in playback["chunks"]] == [
|
||||
(0, 32),
|
||||
(32, 32),
|
||||
(64, 1),
|
||||
]
|
||||
first = (sealed.root / playback["chunks"][0]["path"]).read_bytes()
|
||||
magic, start, count, cells, state_bytes, z_bytes = CHUNK_HEADER.unpack_from(first)
|
||||
assert (magic, start, count, cells) == (CHUNK_MAGIC, 0, 32, 8)
|
||||
assert state_bytes == 32 * 8
|
||||
assert z_bytes == 32 * 8 * 2 * 4
|
||||
assert read_m49_physical_safety_playback(sealed.root).result_id == sealed.result_id
|
||||
|
||||
|
||||
def test_local_api_serves_only_hash_verified_sealed_tracks(tmp_path: Path) -> None:
|
||||
source = _sealed_source(tmp_path / "source")
|
||||
destination = tmp_path / "results"
|
||||
sealed = seal_m49_physical_safety_playback(
|
||||
source_root=source,
|
||||
destination_root=destination,
|
||||
profile_path=(
|
||||
REPOSITORY_ROOT / "config" / "perception" / "m49-physical-safety-shadow-v0.json"
|
||||
),
|
||||
created_at_utc="2026-08-27T12:00:00Z",
|
||||
)
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
build_m49_physical_safety_playback_router(root_provider=lambda: destination)
|
||||
)
|
||||
client = TestClient(app)
|
||||
|
||||
manifest_response = client.get(
|
||||
f"/api/v1/laboratory/m49/physical-safety-playback/{sealed.result_id}/manifest"
|
||||
)
|
||||
assert manifest_response.status_code == 200
|
||||
manifest = manifest_response.json()
|
||||
assert manifest["execution"]["worker_requests_required"] == 0
|
||||
assert manifest["access"] == "read-only-sealed-local"
|
||||
assert "worker" not in manifest["playback"]["centers"]["url"]
|
||||
assert client.get(manifest["playback"]["centers"]["url"]).status_code == 200
|
||||
assert client.get(manifest["playback"]["chunks"][0]["url"]).status_code == 200
|
||||
source_result_id = sealed.manifest["identity"]["source_result_id"]
|
||||
catalog = client.get(
|
||||
"/api/v1/laboratory/m49/physical-safety-playback/results",
|
||||
params={"source_result_id": source_result_id},
|
||||
)
|
||||
assert catalog.status_code == 200
|
||||
assert [item["result_id"] for item in catalog.json()["items"]] == [sealed.result_id]
|
||||
|
||||
chunk_path = sealed.root / sealed.manifest["playback"]["chunks"][0]["path"]
|
||||
damaged = bytearray(chunk_path.read_bytes())
|
||||
damaged[-1] ^= 1
|
||||
chunk_path.write_bytes(damaged)
|
||||
assert client.get(manifest["playback"]["chunks"][0]["url"]).status_code == 503
|
||||
@@ -1,11 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.routing import APIRoute
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from k1link.perception.threat_replay import read_threat_replay_result
|
||||
from k1link.perception.geometry import RecordedGeometryStore
|
||||
from k1link.perception.threat_replay import (
|
||||
read_threat_replay_result,
|
||||
read_threat_replay_result_metadata,
|
||||
)
|
||||
from k1link.perception.threat_timeline import RecordedThreatTimeline
|
||||
from k1link.sessions import RecordedCameraFrame
|
||||
from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
|
||||
|
||||
@@ -128,6 +135,9 @@ def test_m4_6_timeline_is_indexed_and_spatial_evidence_is_chunked() -> None:
|
||||
get_timeline = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}/timeline")
|
||||
get_chunk = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}/timeline/chunk")
|
||||
get_playback = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}/timeline/playback")
|
||||
get_playback_chunk = _endpoint(
|
||||
"/api/v1/laboratory/m4-threat/results/{result_id}/timeline/playback/chunks/{chunk_index}"
|
||||
)
|
||||
|
||||
timeline = get_timeline(RESULT_ID)
|
||||
assert timeline["schema_version"] == "missioncore.recorded-spatial-evidence-timeline/v1"
|
||||
@@ -178,6 +188,33 @@ def test_m4_6_timeline_is_indexed_and_spatial_evidence_is_chunked() -> None:
|
||||
assert playback["track"]["shape"] == [9_207_270, 3]
|
||||
assert playback["track"]["bytes"] == 110_487_240
|
||||
assert len(playback["track"]["sha256"]) == 64
|
||||
assert playback["chunk_frame_count"] == 24
|
||||
assert playback["resident_chunk_count_max"] == 4
|
||||
assert playback["forward_prefetch_chunk_count"] == 1
|
||||
assert len(playback["chunks"]) == 188
|
||||
first_chunk = playback["chunks"][0]
|
||||
assert first_chunk["start"] == 0
|
||||
assert first_chunk["count"] == 24
|
||||
assert first_chunk["bytes"] < 3 * 1024 * 1024
|
||||
chunk_response = get_playback_chunk(RESULT_ID, 0)
|
||||
assert len(chunk_response.body) == first_chunk["bytes"]
|
||||
assert hashlib.sha256(chunk_response.body).hexdigest() == first_chunk["sha256"]
|
||||
|
||||
|
||||
def test_m4_6_timeline_metadata_defers_large_geometry_archives(
|
||||
monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
result = read_threat_replay_result_metadata(RESULTS_ROOT / RESULT_ID)
|
||||
|
||||
def reject_eager_load(*_args: object, **_kwargs: object) -> RecordedGeometryStore:
|
||||
raise AssertionError("timeline metadata eagerly loaded the geometry archives")
|
||||
|
||||
monkeypatch.setattr(RecordedGeometryStore, "from_repository", reject_eager_load)
|
||||
timeline = RecordedThreatTimeline(repository_root=REPOSITORY_ROOT, result=result)
|
||||
|
||||
assert timeline.metadata()["frame_count"] == 4489
|
||||
assert timeline.metadata()["maximum_source_points_per_frame"] == 3092
|
||||
assert timeline._store is None
|
||||
|
||||
|
||||
def test_m4_6_exact_camera_endpoint_is_bound_to_selected_visual_sequence() -> None:
|
||||
|
||||
Reference in New Issue
Block a user