import pytest from k1link.fleet.rover_control import RoverControl @pytest.fixture def hub(): now=[10.] h=RoverControl(lambda:now[0]) h.exchange('node',{'relay_id':'a'*32,'rover':{'supported':True,'instance':'driver','state':'observing'}}) return h,now def arm(h): return h.arm('node',{'standstill_confirmed':True,'current_a':30,'max_erpm':2000})['session_id'] def cmd(id,seq=1,**kw): return {'session_id':id,'sequence':seq,'left':1,'right':1,'stop':False,**kw} def exchange(h,**kw): return h.exchange('node',{'relay_id':'a'*32,'rover':{'supported':True,'instance':'driver',**kw}}) def test_expired_browser_cannot_be_revived_by_late_packet(hub): h,now=hub;id=arm(h);h.command('node',cmd(id));now[0]+=.401 assert exchange(h)['command'] is None with pytest.raises(ValueError):h.command('node',cmd(id,2)) def test_sequence_stop_and_single_owner(hub): h,_=hub;id=arm(h) with pytest.raises(ValueError):arm(h) h.command('node',cmd(id,2)) with pytest.raises(ValueError):h.command('node',cmd(id,1)) assert exchange(h)['command']['sequence']==2 h.command('node',cmd(id,3,stop=True)) with pytest.raises(ValueError):h.command('node',cmd(id,4)) assert exchange(h)['command'] is None @pytest.mark.parametrize('snapshot',[{'state':'receiver'},{'state':'fault'},{'instance':'new-driver'}]) def test_takeover_fault_or_driver_restart_revokes(hub,snapshot): h,_=hub;id=arm(h) assert exchange(h,session_id=id,**snapshot)['command'] is None def test_relay_restart_and_cross_board_do_not_inherit_authority(hub): h,_=hub;id=arm(h) with pytest.raises(ValueError):h.command('other',cmd(id)) assert h.exchange('node',{'relay_id':'b'*32,'rover':{}})['command'] is None @pytest.mark.parametrize('value',[True,float('nan'),float('inf'),1.01,-1.01,'1']) def test_invalid_demand_cannot_refresh(hub,value): h,_=hub;id=arm(h) with pytest.raises(ValueError):h.command('node',cmd(id,left=value)) def test_stale_telemetry_is_unavailable_and_arm_requires_current_board(hub): h,now=hub;now[0]+=1.1 assert h.view('node')['snapshot']=={} with pytest.raises(ValueError):arm(h) def test_stream_keeps_absolute_deadline_and_never_renews_held_frame(hub): h, now = hub identifier = arm(h) h.command('node', cmd(identifier, 1)) first = h.stream('node', 'a'*32) now[0] += .2 repeated = h.stream('node', 'a'*32) assert repeated['command'] == first['command'] assert repeated['control_clock']['monotonic_ms'] > first['control_clock']['monotonic_ms'] assert first['command']['expires_mono_ms'] == pytest.approx(10400) now[0] += .201 assert h.stream('node', 'a'*32)['command'] is None with pytest.raises(ValueError): h.command('node', cmd(identifier, 2)) def test_stream_delivers_new_intent_without_waiting_for_telemetry(hub): h, now = hub identifier = arm(h) h.command('node', cmd(identifier, 1)) h.command('node', cmd(identifier, 2, left=-1)) assert h.stream('node', 'a'*32)['command']['left'] == -1 assert h.stream('node', 'b'*32)['command'] is None h.command('node', cmd(identifier, 3, stop=True)) assert h.stream('node', 'a'*32)['command'] is None def test_new_core_has_distinct_clock_epoch(hub): h, _ = hub assert h.stream('node', 'a'*32)['control_clock']['instance'] != RoverControl().clock_id def test_stream_retires_control_if_return_telemetry_is_lost(hub): h, now = hub identifier = arm(h) for seq in range(1, 13): h.command('node', cmd(identifier, seq)) now[0] += .09 assert h.stream('node', 'a'*32)['command'] is None with pytest.raises(ValueError): h.command('node', cmd(identifier, 13)) def test_intent_wait_wakes_on_update_and_does_not_miss_prior_update(hub): import threading from unittest.mock import patch h, _ = hub identifier = arm(h) previous = h.stream('node', 'a'*32)['command'] entered, done = threading.Event(), threading.Event() def waiter(): entered.set() h.wait_for_intent('node', 'a'*32, previous, timeout=2) done.set() worker = threading.Thread(target=waiter) worker.start() assert entered.wait(1) h.command('node', cmd(identifier, 1)) assert done.wait(.5), 'new intent waited for the periodic keepalive' worker.join(2) # A command arriving after socket write but before wait must not be lost. with patch.object(h.changed, 'wait', side_effect=AssertionError('missed prior update')): h.wait_for_intent('node', 'a'*32, previous, timeout=2) latest = h.stream('node', 'a'*32)['command'] h.command('node', cmd(identifier, 2, stop=True)) with patch.object(h.changed, 'wait', side_effect=AssertionError('missed stop')): h.wait_for_intent('node', 'a'*32, latest, timeout=2)