fix(replay): serve admitted AI overlays without revalidation
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -15,6 +20,44 @@ from k1link.compute.integrated_perception import (
|
||||
)
|
||||
|
||||
|
||||
def _write_result_descriptor(
|
||||
results_root: Path,
|
||||
*,
|
||||
session_id: str,
|
||||
created_at_utc: str,
|
||||
) -> Path:
|
||||
identity = {
|
||||
"schema_version": integrated_module.IDENTITY_SCHEMA,
|
||||
"job_id": f"recorded-camera-{'1' * 24}",
|
||||
"session_id": session_id,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(
|
||||
json.dumps(
|
||||
identity,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
).hexdigest()
|
||||
result_id = f"e10-integrated-perception-{identity_sha256}"
|
||||
root = results_root / result_id
|
||||
root.mkdir()
|
||||
(root / "result.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": integrated_module.RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity": identity,
|
||||
"identity_sha256": identity_sha256,
|
||||
"acceptance_state": "accepted",
|
||||
"publication_scope": "recorded-integrated-realtime-qualification-only",
|
||||
"created_at_utc": created_at_utc,
|
||||
}
|
||||
)
|
||||
)
|
||||
return root
|
||||
|
||||
|
||||
def _worker_modules() -> tuple[object, object]:
|
||||
root = Path(__file__).resolve().parents[1] / "experiments" / "perception"
|
||||
worker = root / "worker"
|
||||
@@ -41,7 +84,7 @@ def _worker_modules() -> tuple[object, object]:
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def test_integrated_overlay_catalog_validation_is_memoized_until_publication(
|
||||
def test_integrated_overlay_recovers_admission_from_sealed_cache_without_revalidation(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -51,32 +94,37 @@ def test_integrated_overlay_catalog_validation_is_memoized_until_publication(
|
||||
cache_root = tmp_path / "cache"
|
||||
for root in (jobs_root, results_root, lidar_packs_root):
|
||||
root.mkdir()
|
||||
job_root = jobs_root / "job-1"
|
||||
job_root.mkdir()
|
||||
first_result = results_root / f"e10-integrated-perception-{'a' * 64}"
|
||||
first_result.mkdir()
|
||||
|
||||
validation_calls = {"job": 0, "result": 0}
|
||||
job = SimpleNamespace(session_id="session-1")
|
||||
|
||||
def validate_job(_root: Path) -> object:
|
||||
validation_calls["job"] += 1
|
||||
return job
|
||||
|
||||
def validate_result(_job_root: Path, candidate: Path, _packs: Path) -> object:
|
||||
validation_calls["result"] += 1
|
||||
return SimpleNamespace(
|
||||
accepted=True,
|
||||
publication_scope="recorded-integrated-realtime-qualification-only",
|
||||
created_at_utc=candidate.name,
|
||||
result_id=candidate.name,
|
||||
result = _write_result_descriptor(
|
||||
results_root,
|
||||
session_id="session-1",
|
||||
created_at_utc="2026-07-29T12:00:00Z",
|
||||
)
|
||||
payload = b"RRF2sealed-overlay"
|
||||
recording_id = "recording-1"
|
||||
cache = cache_root / "session-1" / result.name
|
||||
cache.mkdir(parents=True)
|
||||
output = cache / f"{recording_id}.rrd"
|
||||
output.write_bytes(payload)
|
||||
(cache / f"{recording_id}.rrd.cache.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": integrated_module.OVERLAY_CACHE_SCHEMA,
|
||||
"renderer_version": integrated_module.OVERLAY_RENDERER_VERSION,
|
||||
"result_id": result.name,
|
||||
"recording_id": recording_id,
|
||||
"byte_length": len(payload),
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def reject_revalidation(*_args: object, **_kwargs: object) -> object:
|
||||
raise AssertionError("sealed presentation cache must not revalidate source artifacts")
|
||||
|
||||
monkeypatch.setattr(integrated_module, "validate_camera_compute_job", validate_job)
|
||||
monkeypatch.setattr(
|
||||
integrated_module,
|
||||
"validate_integrated_perception_result",
|
||||
validate_result,
|
||||
reject_revalidation,
|
||||
)
|
||||
store = IntegratedPerceptionOverlayStore(
|
||||
jobs_root=jobs_root,
|
||||
@@ -86,13 +134,75 @@ def test_integrated_overlay_catalog_validation_is_memoized_until_publication(
|
||||
ffmpeg_path=tmp_path / "ffmpeg",
|
||||
)
|
||||
|
||||
assert store._latest("session-1") is not None
|
||||
assert store._latest("session-1") is not None
|
||||
assert validation_calls == {"job": 1, "result": 1}
|
||||
assert (
|
||||
store.render(
|
||||
"session-1",
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id=recording_id,
|
||||
)
|
||||
== payload
|
||||
)
|
||||
admission = json.loads((cache_root / "session-1" / "admission.json").read_text())
|
||||
assert admission["result_id"] == result.name
|
||||
assert store.status("session-1", recording_id=recording_id) == {
|
||||
"state": "ready",
|
||||
"phase": "ready",
|
||||
"elapsed_seconds": pytest.approx(0.0, abs=0.1),
|
||||
"byte_length": len(payload),
|
||||
}
|
||||
|
||||
(results_root / f"e10-integrated-perception-{'b' * 64}").mkdir()
|
||||
assert store._latest("session-1") is not None
|
||||
assert validation_calls == {"job": 2, "result": 3}
|
||||
|
||||
def test_integrated_overlay_serializes_heavy_materialization_across_sessions(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
for name in ("jobs", "results", "lidar-packs"):
|
||||
(tmp_path / name).mkdir()
|
||||
store = IntegratedPerceptionOverlayStore(
|
||||
jobs_root=tmp_path / "jobs",
|
||||
results_root=tmp_path / "results",
|
||||
lidar_packs_root=tmp_path / "lidar-packs",
|
||||
cache_root=tmp_path / "cache",
|
||||
ffmpeg_path=tmp_path / "ffmpeg",
|
||||
)
|
||||
result_id = f"e10-integrated-perception-{'a' * 64}"
|
||||
result = SimpleNamespace(
|
||||
result_id=result_id,
|
||||
created_at_utc="2026-07-29T12:00:00Z",
|
||||
)
|
||||
monkeypatch.setattr(store, "_read_admitted_cache", lambda *_args: None)
|
||||
monkeypatch.setattr(store, "_latest", lambda _session_id: result)
|
||||
monkeypatch.setattr(store, "_write_admission", lambda _result: None)
|
||||
|
||||
active = 0
|
||||
maximum_active = 0
|
||||
active_lock = threading.Lock()
|
||||
|
||||
def render_overlay(*_args: object, **_kwargs: object) -> bytes:
|
||||
nonlocal active, maximum_active
|
||||
with active_lock:
|
||||
active += 1
|
||||
maximum_active = max(maximum_active, active)
|
||||
time.sleep(0.05)
|
||||
with active_lock:
|
||||
active -= 1
|
||||
return b"RRF2materialized"
|
||||
|
||||
monkeypatch.setattr(integrated_module, "_render", render_overlay)
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
results = tuple(
|
||||
executor.map(
|
||||
lambda item: store.render(
|
||||
item,
|
||||
application_id="nodedc_mission_core_recorded",
|
||||
recording_id=f"recording-{item}",
|
||||
),
|
||||
("session-1", "session-2"),
|
||||
)
|
||||
)
|
||||
|
||||
assert results == (b"RRF2materialized", b"RRF2materialized")
|
||||
assert maximum_active == 1
|
||||
|
||||
|
||||
def test_e10_profile_pins_integrated_realtime_budget() -> None:
|
||||
|
||||
@@ -683,6 +683,10 @@ def test_export_preserves_every_decodable_frame_and_source_timeline(tmp_path: Pa
|
||||
assert summary["first_decoded_time_ns"] == 600_000_000
|
||||
assert summary["last_decoded_time_ns"] == 1_700_000_000
|
||||
assert summary["source_sha256"] == _sha256(capture)
|
||||
repeated = export_k1mqtt_to_rrd(capture, tmp_path / "session-repeated.rrd")
|
||||
assert repeated["recording_id"] == summary["recording_id"]
|
||||
assert summary["recording_id"].startswith("k1-")
|
||||
assert len(summary["recording_id"]) == 67
|
||||
assert summary["rrd_bytes"] == output.stat().st_size
|
||||
assert summary["rrd_sha256"] == _sha256(output)
|
||||
assert output.stat().st_size > 0
|
||||
|
||||
@@ -1313,6 +1313,16 @@ def test_recorded_perception_endpoint_returns_one_complete_optional_overlay(
|
||||
calls.append((session_id, application_id, recording_id))
|
||||
return b"RRF2perception"
|
||||
|
||||
def status(self, session_id: str, *, recording_id: str) -> dict[str, object]:
|
||||
assert session_id == session.name
|
||||
assert recording_id == "recording-001"
|
||||
return {
|
||||
"state": "preparing",
|
||||
"phase": "artifact-validation",
|
||||
"elapsed_seconds": 2.5,
|
||||
"byte_length": None,
|
||||
}
|
||||
|
||||
router = build_session_router(store, perception_overlay_provider=Provider())
|
||||
perception_route = endpoint(
|
||||
router,
|
||||
@@ -1334,6 +1344,26 @@ def test_recorded_perception_endpoint_returns_one_complete_optional_overlay(
|
||||
assert calls == [
|
||||
(session.name, "nodedc_mission_core_recorded", "recording-001")
|
||||
]
|
||||
status_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/perception/status",
|
||||
"GET",
|
||||
)
|
||||
status_response = Response()
|
||||
status = asyncio.run(
|
||||
status_route(
|
||||
session_id=session.name,
|
||||
response=status_response,
|
||||
recording_id="recording-001",
|
||||
)
|
||||
)
|
||||
assert status == {
|
||||
"state": "preparing",
|
||||
"phase": "artifact-validation",
|
||||
"elapsed_seconds": 2.5,
|
||||
"byte_length": None,
|
||||
}
|
||||
assert status_response.headers["cache-control"] == "no-store"
|
||||
|
||||
empty_router = build_session_router(store)
|
||||
empty_route = endpoint(
|
||||
|
||||
Reference in New Issue
Block a user