feat(perception): qualify 1x realtime replay envelope
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
@@ -92,6 +93,7 @@ def test_persistent_worker_request_is_single_run_d_backed_and_token_bounded(
|
||||
listen_host="127.0.0.1",
|
||||
listen_port=18020,
|
||||
command="serve",
|
||||
max_duration_seconds=180.0,
|
||||
marker="preserved",
|
||||
)
|
||||
|
||||
@@ -101,6 +103,7 @@ def test_persistent_worker_request_is_single_run_d_backed_and_token_bounded(
|
||||
"request_id": "physical-k1-shadow-001",
|
||||
"output_name": "physical-k1-shadow-001",
|
||||
"token": "a" * 64,
|
||||
"max_duration_seconds": 90,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -108,6 +111,7 @@ def test_persistent_worker_request_is_single_run_d_backed_and_token_bounded(
|
||||
assert run.output == tmp_path / "physical-k1-shadow-001"
|
||||
assert run.token == "a" * 64
|
||||
assert run.token_stdin is False
|
||||
assert run.max_duration_seconds == 90.0
|
||||
assert run.marker == "preserved"
|
||||
assert not hasattr(run, "listen_host")
|
||||
assert not hasattr(run, "output_root")
|
||||
@@ -121,6 +125,16 @@ def test_persistent_worker_request_is_single_run_d_backed_and_token_bounded(
|
||||
"token": "short",
|
||||
},
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="request contract"):
|
||||
module._persistent_run_arguments(
|
||||
service,
|
||||
{
|
||||
"request_id": "physical-k1-shadow-004",
|
||||
"output_name": "physical-k1-shadow-004",
|
||||
"token": "b" * 64,
|
||||
"max_duration_seconds": 181,
|
||||
},
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="request contract"):
|
||||
module._persistent_run_arguments(
|
||||
service,
|
||||
@@ -130,3 +144,28 @@ def test_persistent_worker_request_is_single_run_d_backed_and_token_bounded(
|
||||
"token": "b" * 64,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_telemetry_captures_bounded_queue_snapshots() -> None:
|
||||
module = _module()
|
||||
stream = io.StringIO()
|
||||
telemetry = module._RuntimeTelemetry(
|
||||
stream,
|
||||
interval_seconds=1.0,
|
||||
snapshotters={
|
||||
"detector": lambda: {
|
||||
"capacity": 2,
|
||||
"maximum_depth": 1,
|
||||
"final_depth": 0,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
telemetry._sample()
|
||||
summary = telemetry.summary()
|
||||
|
||||
assert summary["sample_count"] == 1
|
||||
assert summary["final_queues"]["detector"]["capacity"] == 2
|
||||
row = json.loads(stream.getvalue())
|
||||
assert row["schema_version"] == "missioncore.worker-runtime-telemetry/v1"
|
||||
assert row["queues"]["detector"]["maximum_depth"] == 1
|
||||
|
||||
@@ -46,13 +46,25 @@ def test_incremental_media_buffer_is_bounded_and_fail_closed() -> None:
|
||||
source.read(100)
|
||||
|
||||
|
||||
def test_persistent_decoder_rejects_camera_fragment_gaps_before_decode() -> None:
|
||||
def test_persistent_decoder_counts_latest_wins_camera_fragment_gaps() -> 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.feed_segment(_metadata(3), b"third")
|
||||
assert decoder.snapshot()["source_sequence_gaps"] == 1
|
||||
decoder.finish_input()
|
||||
with pytest.raises(ShadowRuntimeError, match="decoder failed"):
|
||||
decoder.join()
|
||||
|
||||
|
||||
def test_persistent_decoder_rejects_non_increasing_camera_sequence() -> None:
|
||||
decoder = PersistentFmp4Decoder(on_frame=lambda _frame: None)
|
||||
decoder.start()
|
||||
decoder.feed_init(b"not-a-real-init")
|
||||
decoder.feed_segment(_metadata(2), b"second")
|
||||
with pytest.raises(ShadowRuntimeError, match="not increasing"):
|
||||
decoder.feed_segment(_metadata(2), b"duplicate")
|
||||
decoder.finish_input()
|
||||
with pytest.raises(ShadowRuntimeError, match="decoder failed"):
|
||||
decoder.join()
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _module(name: str, relative: str):
|
||||
path = Path(__file__).resolve().parents[1] / relative
|
||||
spec = importlib.util.spec_from_file_location(name, 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_camera_replay_loads_segments_lazily_and_checks_each_digest(tmp_path: Path) -> None:
|
||||
module = _module(
|
||||
"e21_source_replay_test",
|
||||
"experiments/perception/run_e12_shadow_transport_replay.py",
|
||||
)
|
||||
epoch = tmp_path / "epoch-1"
|
||||
segments = epoch / "segments"
|
||||
segments.mkdir(parents=True)
|
||||
(epoch / "init.mp4").write_bytes(b"init")
|
||||
rows = []
|
||||
for sequence, payload in ((1, b"one"), (2, b"two")):
|
||||
path = segments / f"{sequence}.m4s"
|
||||
path.write_bytes(payload)
|
||||
rows.append(
|
||||
{
|
||||
"host_epoch_ns": sequence * 100_000_000,
|
||||
"host_monotonic_ns": sequence * 100_000_000,
|
||||
"length": len(payload),
|
||||
"path": f"segments/{sequence}.m4s",
|
||||
"sequence": sequence,
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
}
|
||||
)
|
||||
(epoch / "index.jsonl").write_text(
|
||||
"".join(json.dumps(row) + "\n" for row in rows),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
events = module._camera_events(epoch, 1.0)
|
||||
assert next(events).modality == "camera-init"
|
||||
assert next(events).payload == b"one"
|
||||
(segments / "2.m4s").write_bytes(b"changed-after-generator-start")
|
||||
with pytest.raises(RuntimeError, match="length differs"):
|
||||
next(events)
|
||||
|
||||
|
||||
def test_e21_trend_reports_accumulating_age_and_stable_memory() -> None:
|
||||
module = _module(
|
||||
"e21_analyzer_test",
|
||||
"experiments/perception/analyze_e21_realtime_envelope.py",
|
||||
)
|
||||
growing = module._trend([(float(index), float(index) * 2) for index in range(20)])
|
||||
stable = module._trend([(float(index), 100.0) for index in range(20)])
|
||||
|
||||
assert growing["slope_per_second"] == pytest.approx(2.0)
|
||||
assert growing["late_vs_early_p95"] > 0
|
||||
assert stable["slope_per_second"] == pytest.approx(0.0)
|
||||
assert stable["late_vs_early_p95"] == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_e21_profile_keeps_diagnostic_authority_and_long_1x_gate() -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
profile = json.loads(
|
||||
(
|
||||
root
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "e21_realtime_envelope_profile.json"
|
||||
).read_text()
|
||||
)
|
||||
|
||||
assert profile["source"]["speed"] == 1.0
|
||||
assert profile["source"]["minimum_span_seconds"] >= 55.0
|
||||
assert profile["worker"]["minimum_detector_fps"] >= 9.5
|
||||
assert profile["worker"]["maximum_detector_drop_fraction"] <= 0.02
|
||||
assert profile["worker"]["maximum_p95_world_state_age_ms"] == 200.0
|
||||
assert profile["authority"] == {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import stat
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -97,3 +98,80 @@ def test_shadow_router_accepts_only_validated_diagnostic_results_back() -> None:
|
||||
assert websocket.accepted is True
|
||||
assert published.is_set()
|
||||
assert received == [encoded]
|
||||
|
||||
|
||||
def test_shadow_router_does_not_discard_ingress_while_receiving_results() -> None:
|
||||
class SlowIngress(LivePerceptionIngress):
|
||||
def take_next(
|
||||
self,
|
||||
consumer_id: str,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
):
|
||||
time.sleep(0.01)
|
||||
return super().take_next(consumer_id, timeout=timeout)
|
||||
|
||||
ingress = SlowIngress()
|
||||
received: list[bytes] = []
|
||||
router = build_live_perception_shadow_router(
|
||||
ingress,
|
||||
"test-plugin",
|
||||
bearer_token="x" * 43,
|
||||
result_receiver=lambda payload: not received.append(payload),
|
||||
)
|
||||
ingress.begin_session("session-1")
|
||||
for modality, sequence in (
|
||||
("camera-init", 0),
|
||||
("camera-frame", 1),
|
||||
("lidar", 1),
|
||||
("pose", 1),
|
||||
):
|
||||
assert ingress.publish(
|
||||
modality=modality,
|
||||
source_id="sensor.camera.right",
|
||||
source_sequence=sequence,
|
||||
captured_at_epoch_ns=sequence,
|
||||
received_monotonic_ns=sequence,
|
||||
payload=modality.encode(),
|
||||
)
|
||||
ingress.end_session("session-1")
|
||||
expected_events = sum(
|
||||
int(queue["depth"]) for queue in ingress.snapshot()["queues"].values()
|
||||
)
|
||||
|
||||
class DuplexFakeWebSocket:
|
||||
def __init__(self) -> None:
|
||||
self.headers = {"authorization": f"Bearer {'x' * 43}"}
|
||||
self.results_remaining = 8
|
||||
self.sent: list[bytes] = []
|
||||
|
||||
async def accept(self) -> None:
|
||||
return
|
||||
|
||||
async def receive(self) -> dict[str, object]:
|
||||
if self.results_remaining:
|
||||
self.results_remaining -= 1
|
||||
await asyncio.sleep(0)
|
||||
return {"type": "websocket.receive", "bytes": b"result"}
|
||||
deadline = asyncio.get_running_loop().time() + 1
|
||||
while len(self.sent) < expected_events:
|
||||
if asyncio.get_running_loop().time() >= deadline:
|
||||
break
|
||||
await asyncio.sleep(0.005)
|
||||
return {"type": "websocket.disconnect"}
|
||||
|
||||
async def send_bytes(self, payload: bytes) -> None:
|
||||
self.sent.append(payload)
|
||||
|
||||
async def close(self, **_: object) -> None:
|
||||
return
|
||||
|
||||
websocket = DuplexFakeWebSocket()
|
||||
asyncio.run(router.routes[0].endpoint(websocket)) # type: ignore[attr-defined]
|
||||
|
||||
assert len(received) == 8
|
||||
assert len(websocket.sent) == expected_events
|
||||
snapshot = ingress.snapshot()
|
||||
assert sum(
|
||||
int(queue["consumed"]) for queue in snapshot["queues"].values()
|
||||
) == expected_events
|
||||
|
||||
Reference in New Issue
Block a user