Preserve the completed teach-and-repeat laboratory stage: reference preparation, cascaded acquisition, local tracking and recovery, recording lifecycle, replay qualification, and persistent Rerun scene controls. Document the open grid-picking regression and Rerun upgrade contract. No autonomous driving or loop-closure optimization is claimed.
313 lines
13 KiB
Python
313 lines
13 KiB
Python
"""Bounded functional tests of display/registration separation and delta fences."""
|
|
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from test_planning_live import event, fixture_service
|
|
from test_planning_stabilization import fit, sample
|
|
|
|
from k1link.missions.live_buffer import LiveCloudBuffer
|
|
from k1link.missions.live_display_buffer import LiveDisplayBuffer
|
|
from k1link.missions.live_scene_delta import decode_cursor, scene_delta
|
|
from k1link.web.planning_live_api import build_planning_live_router
|
|
|
|
|
|
def ingest(display, numeric, t, sequence, points=None):
|
|
pose = event("pose", t=t, sequence=sequence, p=(t / 100, 0, 0))
|
|
cloud = event(
|
|
"points",
|
|
t=t + 0.001,
|
|
sequence=sequence + 1,
|
|
points=np.array([[t / 100, 1, 0]]) if points is None else points,
|
|
)
|
|
for e in (pose, cloud):
|
|
numeric.ingest(e)
|
|
display.ingest(e, numeric)
|
|
return cloud
|
|
|
|
|
|
def test_display_preserves_high_points_independently_from_tracking_filter():
|
|
numeric, display = LiveCloudBuffer([[0,0,0],[10,0,0]]), LiveDisplayBuffer()
|
|
points = np.array([[1,1,1],[1,0,40],[1,0,79.9],[1,0,80.1]])
|
|
ingest(display, numeric, 100, 1, points)
|
|
np.testing.assert_allclose(display.snapshot()["points"], points[:3])
|
|
np.testing.assert_allclose(numeric.snapshot()["points"], points[:1])
|
|
|
|
|
|
def test_native_packets_are_not_numerical_sampling_and_snapshots_are_immutable():
|
|
numeric, display = LiveCloudBuffer([[0, 0, 0], [10, 0, 0]]), LiveDisplayBuffer()
|
|
ingest(display, numeric, 100, 1)
|
|
first = display.snapshot()
|
|
for i in range(1, 10):
|
|
ingest(display, numeric, 100 + i / 10, i * 2 + 1)
|
|
assert display.frames == 10
|
|
assert len(numeric.chunks) == 2 # registration's 500 ms policy is unchanged
|
|
assert display.snapshot()["sequence"] == 20
|
|
assert first["sequence"] == 2 and first["cloud_revision"] == 1
|
|
np.testing.assert_equal(first["points"], [[1, 1, 0]])
|
|
assert len(display.chunks) == 1 # The current half-second receipt stays live.
|
|
np.testing.assert_allclose(display.snapshot()["current_points"], [[1.009, 1, 0]])
|
|
|
|
|
|
def test_budgets_eviction_tombstones_old_receipts_and_segment_reset():
|
|
numeric, display = LiveCloudBuffer([[0, 0, 0], [10, 0, 0]]), LiveDisplayBuffer()
|
|
points = np.array([[x * 0.3, y * 0.3, 0] for x in range(35) for y in range(35)])
|
|
for i in range(60):
|
|
ingest(display, numeric, 100 + i * 0.5, 2 * i, points)
|
|
assert len(display.chunks) <= 40
|
|
assert sum(map(len, display.chunks.values())) <= 40_000
|
|
before = display.frames
|
|
display.ingest(event("points", t=100, points=points), numeric)
|
|
assert display.frames == before
|
|
assert any(p is None and revision > 0 for _, revision, p in display.snapshot()["chunks"])
|
|
ingest(display, numeric, 133, 200, points)
|
|
assert numeric.segment == 1 and len(display.chunks) == 0
|
|
assert len(display.snapshot()["current_points"]) == len(points)
|
|
|
|
|
|
def test_delta_sends_only_changed_chunks_pose_and_transform(monkeypatch):
|
|
import k1link.missions.live_scene_delta as module
|
|
|
|
logs = []
|
|
|
|
class Recording:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
def binary_stream(self):
|
|
return SimpleNamespace(read=lambda: b"RRF2")
|
|
|
|
def log(self, name, value, **kwargs):
|
|
logs.append((name, value, kwargs))
|
|
|
|
def set_time(self, *args, **kwargs):
|
|
logs.append(("time", args, kwargs))
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
def disconnect(self):
|
|
pass
|
|
|
|
monkeypatch.setattr(module.rr, "RecordingStream", Recording)
|
|
monkeypatch.setattr(module, "log_base", lambda *args: None)
|
|
monkeypatch.setattr(module, "log_view", lambda *args: None)
|
|
monkeypatch.setattr(module.rr, "Points3D", lambda points, **kwargs: np.asarray(points))
|
|
monkeypatch.setattr(module.rr, "Transform3D", lambda **kwargs: kwargs)
|
|
numeric, display = LiveCloudBuffer([[0, 0, 0], [10, 0, 0]]), LiveDisplayBuffer()
|
|
ingest(display, numeric, 100, 1)
|
|
|
|
def render(cursor="", result=None, live=True, evidence=None, options=None):
|
|
return scene_delta(
|
|
"run",
|
|
display.epoch,
|
|
np.zeros((3, 3)),
|
|
np.zeros((2, 3)),
|
|
display.snapshot(),
|
|
result or fit(),
|
|
evidence,
|
|
live,
|
|
cursor=cursor,
|
|
options=options,
|
|
)
|
|
|
|
_, cursor = render()
|
|
assert all(
|
|
kwargs.get("static")
|
|
for name, _, kwargs in logs
|
|
if name == "world/query" or "/cloud/" in name
|
|
)
|
|
np.testing.assert_equal(dict((n, v) for n, v, _ in logs)["world/query/live"], [[1, 1, 0]])
|
|
logs.clear()
|
|
render(options={"ceiling_m": -0.1})
|
|
assert len(dict((n, v) for n, v, _ in logs)["world/query/live"]) == 0
|
|
logs.clear()
|
|
assert render(cursor)[0] == b""
|
|
assert logs == []
|
|
ingest(display, numeric, 100.1, 3)
|
|
_, cursor = render(cursor)
|
|
assert [n for n, _, _ in logs if "/cloud/" in n] == []
|
|
assert "world/query/live" in [n for n, _, _ in logs]
|
|
assert "world/query" not in [n for n, _, _ in logs]
|
|
logs.clear()
|
|
adjusted = fit()
|
|
adjusted["T_reference_query"][0][3] = 9
|
|
_, cursor = render(cursor, result=adjusted)
|
|
assert [n for n, _, _ in logs] == ["world/query", "time", "world/query/live"]
|
|
assert logs[0][1]["translation"][0] == 9
|
|
logs.clear()
|
|
_, cursor = render(cursor, result=adjusted, evidence=(sample(), fit()))
|
|
assert "world/validated_query" in [n for n, _, _ in logs]
|
|
logs.clear()
|
|
render(cursor, result=adjusted, live=False)
|
|
assert "world/validated_query" in [n for n, _, _ in logs]
|
|
assert not any("/cloud/" in n for n, _, _ in logs)
|
|
|
|
|
|
def test_presentation_changes_and_geometry_repairs_never_reset_camera(monkeypatch):
|
|
import k1link.missions.live_scene_delta as module
|
|
|
|
views = []
|
|
monkeypatch.setattr(module, "log_view", lambda *args: views.append(args[-1].copy()))
|
|
reference = np.array([[0., 0., -1.], [20., 1., 40.]])
|
|
cursor = ""
|
|
|
|
def render(options=None, **kwargs):
|
|
nonlocal cursor
|
|
payload, cursor = scene_delta(
|
|
"camera-test", kwargs.pop("epoch", "first"), reference, reference,
|
|
None, None, None, False, cursor=cursor, options=options, **kwargs,
|
|
)
|
|
return payload
|
|
|
|
assert render().startswith(b"RRF2")
|
|
assert len(views) == 1
|
|
for options in ({"ceiling_m": 3}, {"reference": False}, {"query": False},
|
|
{"trajectory": False}, {"point_size": 4}, {"grid": False}, {}):
|
|
assert render(options).startswith(b"RRF2")
|
|
assert len(views) == 1
|
|
assert render(base=True).startswith(b"RRF2")
|
|
assert render(epoch="reconnected").startswith(b"RRF2")
|
|
assert len(views) == 1
|
|
render({"mode": "top"})
|
|
render({"mode": "top", "reset": 1})
|
|
assert len(views) == 3
|
|
assert render({"mode": "top", "reset": 1}) == b""
|
|
assert len(views) == 3
|
|
|
|
|
|
def test_grid_is_display_geometry_and_never_a_camera_command():
|
|
from k1link.missions.live_scene import grid_lines, log_base
|
|
|
|
reference = np.array([[0., 0., -1.], [20., 1., 40.]])
|
|
logs = {}
|
|
recording = SimpleNamespace(log=lambda path, value, **kw: logs.update({path: value}))
|
|
log_base(recording, reference, reference, {"ceiling_m": 3})
|
|
assert "world/grid" in logs
|
|
assert len(logs["world/reference"].positions.as_arrow_array()) == 1
|
|
assert len(grid_lines(reference)) > 0
|
|
log_base(recording, reference, reference, {"grid": False})
|
|
assert len(logs["world/grid"].strips.as_arrow_array()) == 0
|
|
|
|
|
|
def test_service_freezes_after_loss_fences_identity_and_expires_without_packets(
|
|
tmp_path, monkeypatch
|
|
):
|
|
import k1link.missions.live_tests as module
|
|
|
|
service, source, _, _ = fixture_service(tmp_path, monkeypatch)
|
|
run_id = "cedf3261-a703-453d-a1c8-aac71c6ce5e3"
|
|
service.run = dict(
|
|
id=run_id,
|
|
state="running",
|
|
query_session_id="B",
|
|
query_generation=2,
|
|
vehicle_control=False,
|
|
)
|
|
service.source = source
|
|
source.state.update(active=True, session_id="B", session_generation=2)
|
|
service.reference, service.reference_path = np.zeros((3, 3)), np.zeros((2, 3))
|
|
summary = service.get()
|
|
assert summary["scene_height_min_m"] == 0.0
|
|
assert summary["scene_height_max_m"] == 80.0
|
|
clock = [int(100.1e9)]
|
|
monkeypatch.setattr(module, "time", SimpleNamespace(monotonic_ns=lambda: clock[0]))
|
|
monkeypatch.setattr(service, "persist", lambda: None)
|
|
numeric = LiveCloudBuffer(service.reference_path)
|
|
for e in (event("pose", t=100), event("points", t=100.01, points=np.ones((2, 3)))):
|
|
numeric.ingest(e)
|
|
service.observe_display(e, numeric, clock[0])
|
|
assert service.presentation.sample is None # no hint fallback
|
|
service.commit_result(
|
|
fit(),
|
|
sample(),
|
|
{"accepted": True},
|
|
"tracking",
|
|
phase="tracking",
|
|
message="tracking",
|
|
tracking_established=True,
|
|
)
|
|
app = FastAPI()
|
|
app.include_router(build_planning_live_router(service))
|
|
with TestClient(app) as client:
|
|
url = f"/api/v1/mission-planner/live-tests/{run_id}/scene-delta.rrd"
|
|
first = client.get(url)
|
|
assert first.status_code == 200 and first.content.startswith(b"RRF2")
|
|
cursor = first.headers["X-Planning-Scene-Cursor"]
|
|
assert first.headers["X-Planning-Cloud-Revision"] == "1"
|
|
assert first.headers["X-Planning-Cloud-Sequence"] == "1"
|
|
assert first.headers["X-Planning-Height-Min"] == "0.0"
|
|
assert first.headers["X-Planning-Height-Max"] == "80.0"
|
|
observation = {
|
|
"schema_version": "missioncore.planning-browser-presentation/v1",
|
|
"samples": [
|
|
{
|
|
"cloud_revision": 1,
|
|
"cloud_sequence": 1,
|
|
"display_epoch": service.display.epoch,
|
|
"request_ms": 40.0,
|
|
"rerun_admission_ms": 2.0,
|
|
"first_animation_frame_ms": 12.0,
|
|
"second_animation_frame_ms": 28.0,
|
|
"frame_timeout": False,
|
|
"source_to_second_animation_frame_upper_bound_ms": 268.0,
|
|
}
|
|
],
|
|
}
|
|
report_url = (
|
|
f"/api/v1/mission-planner/live-tests/{run_id}/presentation-observations"
|
|
)
|
|
assert client.post(report_url, json=observation).status_code == 204
|
|
presentation = service.get()["browser_presentation"]
|
|
assert presentation["reported_sample_count"] == 1
|
|
assert presentation["second_animation_frame_ms"]["p50"] == 28.0
|
|
assert service.get()["vehicle_control"] is False
|
|
observation["samples"][0]["cloud_revision"] = 2
|
|
assert client.post(report_url, json=observation).status_code == 204
|
|
assert service.get()["browser_presentation"]["rejected_sample_count"] == 1
|
|
assert client.get(url, params={"cursor": cursor}).status_code == 204
|
|
assert client.get(url, params={"cursor": "corrupt"}).status_code == 200
|
|
assert client.get(url, params={"cursor": "a" * 2049}).status_code == 422
|
|
clock[0] = int(102.2e9)
|
|
expired = client.get(url, params={"cursor": cursor})
|
|
assert expired.status_code == 200
|
|
assert decode_cursor(expired.headers["X-Planning-Scene-Cursor"])["evidence"] is None
|
|
frozen = service.presentation.sample
|
|
service.accepted_sample = None
|
|
fresh = event("points", t=102.3, points=np.ones((2, 3)))
|
|
service.observe_display(replace(fresh, session_id="other"), numeric, int(102.3e9))
|
|
assert service.display.frames == 1
|
|
numeric.ingest(event("pose", t=102.2))
|
|
service.observe_display(event("pose", t=102.2), numeric, int(102.2e9))
|
|
service.observe_display(fresh, numeric, int(102.3e9))
|
|
assert service.presentation.sample is frozen
|
|
source.state["session_generation"] = 3
|
|
assert not service._view_live(int(102.3e9))
|
|
|
|
|
|
def test_reinitialize_endpoint_discards_only_a_failed_initialization(tmp_path, monkeypatch):
|
|
service, _, _, _ = fixture_service(tmp_path, monkeypatch)
|
|
run_id = "cedf3261-a703-453d-a1c8-aac71c6ce5e4"
|
|
service.run = dict(
|
|
id=run_id,
|
|
state="running",
|
|
planning_phase="lost",
|
|
tracking_established=False,
|
|
initialization_attempt=1,
|
|
reinitialization_count=0,
|
|
)
|
|
service.persist = lambda: None
|
|
app = FastAPI()
|
|
app.include_router(build_planning_live_router(service))
|
|
with TestClient(app) as client:
|
|
response = client.post(f"/api/v1/mission-planner/live-tests/{run_id}/reinitialize")
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["state"] == "running"
|
|
assert body["planning_phase"] == "waiting-cloud"
|
|
assert body["initialization_attempt"] == 2
|
|
assert body["reinitialization_count"] == 1
|