feat: finalize corrected-route planning and Rerun recording review
This commit is contained in:
@@ -44,6 +44,77 @@ def initialized():
|
||||
return boot, result
|
||||
|
||||
|
||||
def long_route_search():
|
||||
from k1link.missions.route_relocalization import ROUTE_RELOCALIZATION_POLICY
|
||||
boot, result = initialized()
|
||||
boot.initialization_policy = ROUTE_RELOCALIZATION_POLICY
|
||||
result["initialization"].update(policy=ROUTE_RELOCALIZATION_POLICY,
|
||||
scope="selected-route", expected_attempts=108)
|
||||
for i in range(20, 200):
|
||||
boot.ingest(event(i * .5, i * 2 + 1), 0)
|
||||
boot.ingest(event(i * .5 + .001, i * 2 + 2, kind="points"), 0)
|
||||
boot.tick(int((i * .5 + 1.01) * 1e9), 0)
|
||||
assert boot.phase == "searching"
|
||||
return boot, result
|
||||
|
||||
|
||||
def test_long_complete_search_is_only_a_hypothesis_until_three_new_windows():
|
||||
boot, result = long_route_search()
|
||||
ready = 101_000_000_000
|
||||
decision = boot.offer_prior(result, ready, 0)
|
||||
assert decision["age_s"] > 90
|
||||
assert decision["provisional"] and decision["stationary_search_continuity"]
|
||||
assert not decision["accepted"] and boot.gate.matrix is None
|
||||
seen = set()
|
||||
for i, start in enumerate((101., 106., 111.)):
|
||||
sample, _ = fresh_window(boot, start, 500 + i * 20, segment=0)
|
||||
ids = {e["sequence"] for e in sample["events"]}
|
||||
assert not ids & seen
|
||||
seen |= ids
|
||||
assert all(e["monotonic_ns"] > ready for e in sample["events"])
|
||||
decision = boot.accept_fresh(fit(), sample, int((start + 4.2) * 1e9), 0)
|
||||
assert decision["accepted"]
|
||||
assert boot.phase == ("tracking" if i == 2 else "validating")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"failure", ["motion", "gap", "pose-only", "cloud-only", "silent", "identity"]
|
||||
)
|
||||
def test_long_search_cannot_survive_motion_or_invalid_live_receipts(failure):
|
||||
boot, result = long_route_search()
|
||||
now = 111_000_000_000
|
||||
if failure == "motion":
|
||||
boot.ingest(replace(event(100., 500), position=(.11, 0., 0.)), 0)
|
||||
elif failure == "gap":
|
||||
boot.ingest(event(100., 500), 1)
|
||||
elif failure == "identity":
|
||||
with pytest.raises(ValueError):
|
||||
boot.ingest(replace(event(100., 500), generation=2), 0)
|
||||
elif failure in {"pose-only", "cloud-only"}:
|
||||
boot.ingest(event(109., 500, kind="pose" if failure == "pose-only" else "points"), 0)
|
||||
boot.tick(now, boot.segment)
|
||||
decision = boot.offer_prior(result, now, boot.segment)
|
||||
assert boot.phase == "lost" and not decision["provisional"]
|
||||
assert boot.gate.matrix is None
|
||||
|
||||
|
||||
def test_long_search_can_try_next_place_only_with_another_fresh_window():
|
||||
boot, result = long_route_search()
|
||||
alternative = np.eye(4)
|
||||
alternative[0, 3] = 40
|
||||
result["initialization"]["candidate_queue"] = [
|
||||
dict(candidate_index=1, T_reference_query=np.eye(4).tolist(), ambiguous=False),
|
||||
dict(candidate_index=2, T_reference_query=alternative.tolist(), ambiguous=False),
|
||||
]
|
||||
assert boot.offer_prior(result, 101_000_000_000, 0)["provisional"]
|
||||
sample, _ = fresh_window(boot, 101., 500, segment=0)
|
||||
decision = boot.accept_fresh(dict(status="rejected"), sample, 105_200_000_000, 0)
|
||||
assert not decision["accepted"] and decision["next_candidate_trial"] == 2
|
||||
assert boot.gate.matrix is None and boot.floor_ns == 105_200_000_000
|
||||
assert np.allclose(boot.prior, alternative)
|
||||
assert boot.validation(106_000_000_000, 0) is None
|
||||
|
||||
|
||||
def fresh_window(boot, start, seq, segment=1):
|
||||
for i in range(7):
|
||||
boot.ingest(event(start + i * 0.5, seq + i * 2), segment)
|
||||
@@ -170,8 +241,9 @@ def test_initial_failure_and_later_loss_have_distinct_operator_messages():
|
||||
rejected = boot.offer_prior(result, 36_000_000_000, 0)
|
||||
assert rejected["reason"] == "initialization-incomplete"
|
||||
assert not boot.tracking_established
|
||||
assert "Синхронизация маршрута не завершилась" in phase_message(boot)
|
||||
assert "Остановите устройство и запись" in phase_message(boot)
|
||||
assert "Поиск не завершён" in phase_message(boot)
|
||||
assert "Переинициализировать" in phase_message(boot)
|
||||
assert "Остановите устройство" not in phase_message(boot)
|
||||
assert "потеряна" not in phase_message(boot)
|
||||
boot.tracking_established = True
|
||||
boot.stop("stale")
|
||||
@@ -266,7 +338,7 @@ def test_candidate_queue_never_bypasses_freshness_identity_or_ambiguity(failure)
|
||||
ready = 45_000_000_000 if failure == "expired" else 25_000_000_000
|
||||
if failure == "ambiguous":
|
||||
result["initialization"]["candidate_queue"][1]["ambiguous"] = True
|
||||
# Use the route policy's existing 35 s search fence for the late prefix case.
|
||||
# The legacy local protocol still has its 30 s search / 40 s source fences.
|
||||
if failure == "expired":
|
||||
boot.search_started_ns += 5_000_000_000
|
||||
assert boot.offer_prior(result, ready, 1)["provisional"]
|
||||
|
||||
Reference in New Issue
Block a user