feat: finalize corrected-route planning and Rerun recording review
This commit is contained in:
@@ -186,11 +186,13 @@ def test_receipt_gap_is_not_an_instantaneous_jump_and_clears_fit_window():
|
||||
b.ingest(event('pose',t=11.1,p=(21,0,0)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("loss", ["stale", "reference-coverage", "fit-rejected", "worker-error", "pose-jump", "operator-stop", "operator-stop-pending"])
|
||||
@pytest.mark.parametrize("loss", ["stale", "reference-coverage", "fit-rejected", "worker-error", "pose-jump", "operator-stop", "operator-stop-pending", "multi-lap"])
|
||||
def test_live_stationary_bootstrap_keeps_calibration_separate_and_requires_fresh_windows(tmp_path,monkeypatch,loss):
|
||||
import k1link.missions.live_tests as module
|
||||
from k1link.missions.route_relocalization import ROUTE_RELOCALIZATION_POLICY
|
||||
service,source,lock,_=fixture_service(tmp_path,monkeypatch)
|
||||
service,source,lock,draft=fixture_service(tmp_path,monkeypatch)
|
||||
if loss == "multi-lap":
|
||||
draft['route'].update(length_m=4, end_index=4, points=draft['route']['points'][:5])
|
||||
clock=[100.]
|
||||
monkeypatch.setattr(module,'time',SimpleNamespace(
|
||||
monotonic=lambda:clock[0],monotonic_ns=lambda:int(clock[0]*1e9)))
|
||||
@@ -248,7 +250,12 @@ def test_live_stationary_bootstrap_keeps_calibration_separate_and_requires_fresh
|
||||
until(lambda:service.get()['planning_phase']=='searching')
|
||||
# Capture continues while the worker searches; an actual post-ready
|
||||
# receipt gap must invalidate the prior, not be hidden by this fixture.
|
||||
for i in range(40):frame(170+i*.5)
|
||||
for i in range(40):
|
||||
# Receipts cannot be ahead of the live clock during a continuity
|
||||
# check. Drain each fixture frame at its actual delivery time.
|
||||
clock[0] = 170 + i * .5 + .01
|
||||
frame(170 + i * .5)
|
||||
until(lambda:source.queue.empty())
|
||||
until(lambda:source.queue.empty())
|
||||
clock[0]=190
|
||||
entry_release.set()
|
||||
@@ -271,6 +278,31 @@ def test_live_stationary_bootstrap_keeps_calibration_separate_and_requires_fresh
|
||||
assert service._scene_result['matched_query_indices']==([0] if j==2 else [])
|
||||
assert calls==['entry','fresh','fresh','fresh']
|
||||
assert service.get()['planning_phase']=='tracking'
|
||||
if loss == "multi-lap":
|
||||
# Exercise the real bootstrap/gate as well as the loop. The fit is
|
||||
# synthetic, but fresh windows must continue beyond the old cap.
|
||||
service.update(maximum_distance_m=4)
|
||||
begin = clock[0] + .5
|
||||
for i in range(1, 25):
|
||||
leg, offset = divmod(i - 1, 8)
|
||||
position = (offset + 1) * .5 if leg % 2 == 0 else 4 - (offset + 1) * .5
|
||||
clock[0] = begin + i * .5 + .002
|
||||
frame(begin + i * .5, p=(position, 0, 0))
|
||||
until(source.queue.empty)
|
||||
until(lambda: service.get()['distance_m'] == 12)
|
||||
begin = clock[0] + .5
|
||||
for i in range(12):
|
||||
clock[0] = begin + i * .5 + .002
|
||||
frame(begin + i * .5, p=(4, 0, 0))
|
||||
until(source.queue.empty)
|
||||
until(lambda: calls.count('fresh') >= 6)
|
||||
assert service.get()['state'] == 'running'
|
||||
assert service.get()['planning_phase'] == 'tracking'
|
||||
assert service.accepted_sample is not None
|
||||
assert service.get().get('recovery_attempt', 0) == 0
|
||||
assert service.get().get('termination_reason') is None
|
||||
assert source.state['active'] and lock.locked()
|
||||
return
|
||||
if loss in {"operator-stop", "operator-stop-pending"}:
|
||||
if loss == "operator-stop-pending":
|
||||
fail_next[0] = loss
|
||||
@@ -360,7 +392,7 @@ def test_operator_can_retry_a_failed_route_identification_without_stopping_captu
|
||||
monkeypatch.setattr(module,'time',SimpleNamespace(
|
||||
monotonic=lambda:clock[0],monotonic_ns=lambda:int(clock[0]*1e9)))
|
||||
searches=[]
|
||||
def rejected_entry(*_args):
|
||||
def rejected_entry(*_args, **_kwargs):
|
||||
searches.append('entry')
|
||||
return dict(status='rejected',T_reference_query=np.eye(4).tolist(),matched_query_indices=[],
|
||||
overlap=.6,inlier_rmse_m=.28,reasons=['Большое расстояние между поверхностями.'],
|
||||
@@ -439,3 +471,49 @@ def test_stationary_cancel_while_searching_does_not_publish_late_prior(tmp_path,
|
||||
release.set();service.close()
|
||||
assert service.accepted_sample is None and service.get()['result'] is None
|
||||
assert not lock.locked() and source.owner is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cause", ["operator-stop", "motion", "source-ended"])
|
||||
def test_long_search_receives_cancellation_without_waiting_for_total_search(tmp_path,monkeypatch,cause):
|
||||
import k1link.missions.live_tests as module
|
||||
from k1link.missions.route_relocalization_worker import incomplete_result
|
||||
service,source,lock,_=fixture_service(tmp_path,monkeypatch)
|
||||
clock=[100.]
|
||||
entered, cancelled = threading.Event(), threading.Event()
|
||||
monkeypatch.setattr(module,'time',SimpleNamespace(
|
||||
monotonic=lambda:clock[0],monotonic_ns=lambda:int(clock[0]*1e9)))
|
||||
def search(*args, cancel_event, **kwargs):
|
||||
entered.set()
|
||||
assert cancel_event.wait(3), 'Planner did not cancel its own search child'
|
||||
cancelled.set()
|
||||
return incomplete_result('worker-cancelled')
|
||||
monkeypatch.setattr(module,'run_route_relocalization',search)
|
||||
run=service.start('draft',1)
|
||||
points=np.random.default_rng(11).uniform([-1,-3,-1],[8,3,3],(1500,3))
|
||||
try:
|
||||
until(lambda:service.get()['state']=='waiting')
|
||||
source.state.update(active=True,session_id='B',session_generation=2)
|
||||
for i in range(20):
|
||||
source.queue.put(event('pose',t=100+i*.5,sequence=i*2+1))
|
||||
source.queue.put(event('points',t=100+i*.5+.001,sequence=i*2+2,points=points))
|
||||
until(source.queue.empty)
|
||||
clock[0]=110.01
|
||||
until(entered.is_set)
|
||||
if cause=='operator-stop':
|
||||
service.stop(run['id'])
|
||||
elif cause=='source-ended':
|
||||
source.state['active']=False
|
||||
else:
|
||||
source.queue.put(event('pose',t=110.005,sequence=41,p=(.2,0,0)))
|
||||
until(lambda:service.get()['planning_phase']=='lost')
|
||||
until(cancelled.is_set)
|
||||
assert service.accepted_sample is None
|
||||
if cause=='motion':
|
||||
assert service.get()['state']=='running' and source.state['active']
|
||||
service.request_reinitialization(run['id'])
|
||||
until(lambda:service.get()['planning_phase']=='waiting-cloud')
|
||||
assert service.get()['initialization_attempt']==2
|
||||
finally:
|
||||
source.state['active']=False
|
||||
service.close()
|
||||
assert not lock.locked() and source.owner is None
|
||||
|
||||
Reference in New Issue
Block a user