121 lines
7.7 KiB
Python
121 lines
7.7 KiB
Python
"""Independent native route policy regression; no input/display operations."""
|
|
import copy
|
|
from pathlib import Path
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import patch
|
|
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
|
|
import prison_approach_evidence as v
|
|
|
|
|
|
def fixture():
|
|
samples={}
|
|
for i in range(21,36):
|
|
moved=min(6,max(0,i-24))*.15
|
|
p=[6092.77,211.44,7157.1-moved]
|
|
active=25<=i<=29
|
|
samples[i]={'sample':i,'real_ms':1000+i*100,
|
|
'input':{'origin':p,'forward':1. if active else 0.,'input':[0,0,1 if active else 0],'velocity':[0,0,-1.5 if active else 0]},
|
|
'look':{'direction':[0,0,-1]},
|
|
'action':{'pressed':0.,'performing':False,'progress':0.},
|
|
'geometry':{'body':p,'prison':[6090,211.44,7160],'gate':[6090,211.34,7154],'context':[6092.77,213.22,7154],'gate_forward':[0,0,1],'door_state':0.,'door_control':0.}}
|
|
data={'snapshots':samples,'ready':{'sample':20,'real_ms':3000}}
|
|
look={'return_sample':20,'finished_monotonic':9.}
|
|
proof={'final_sample':35,'requested':True,'completed':True,'nonce':'N','paired_release_errors':[],'unconfirmed_held_key':None,'started_monotonic':10.,'finished_monotonic':12.,'deadline_monotonic':35.,'events':[{'key':'W','down':True,'monotonic':10.5},{'key':'W','down':False,'monotonic':10.8}],'stops':[{'before_sample':24,'after_sample':33}],'motions':[],'samples':[{'sample':i,'real_ms':1000+i*100,'received_monotonic':10+(i-21)*.1} for i in range(21,36)]}
|
|
return data,look,proof
|
|
|
|
|
|
class RouteEvidence(unittest.TestCase):
|
|
def run_case(self, mutate=None, geometry=True, calibration=True):
|
|
d,l,p=fixture()
|
|
if mutate:mutate(d,l,p)
|
|
with patch.object(v,'join_gate_snapshots',return_value=d),patch.object(v,'validate_gate_geometry',return_value={'verified':geometry,'errors':[]}),patch.object(v,'validate_look_evidence',return_value={'verified':calibration,'errors':[],'response_yaw_degrees':2.}) as validator:
|
|
r=v.validate_approach_evidence('raw','N',l,p)
|
|
if r['verified']:self.assertEqual(validator.call_args.args[-1],p['events'][:2])
|
|
return r
|
|
def reject(self, mutation):
|
|
r=self.run_case(mutation);self.assertFalse(r['verified'],r);self.assertTrue(r['errors'])
|
|
def test_stopped_interior_route_is_limited_not_escape(self):
|
|
r=self.run_case();self.assertTrue(r['verified'],r);self.assertAlmostEqual(r['path_metres'],.9);self.assertAlmostEqual(r['final_distance_metres'],.7);self.assertFalse(r['escape_verified']);self.assertFalse(r['held_action_verified'])
|
|
def test_no_helper_completion_or_release_shortcut(self):
|
|
for field,value in [('completed',False),('nonce','foreign'),('paired_release_errors',['failed']),('unconfirmed_held_key','W')]:
|
|
self.reject(lambda d,l,p:p.update({field:value}))
|
|
def test_requires_both_independent_raw_gate_and_calibration_verification(self):
|
|
self.assertFalse(self.run_case(geometry=False)['verified']);self.assertFalse(self.run_case(calibration=False)['verified'])
|
|
def test_actual_held_time_and_key_shape_not_requested_sleep(self):
|
|
self.reject(lambda d,l,p:p['events'][1].update(monotonic=11.))
|
|
self.reject(lambda d,l,p:p['events'][0].update(key='F'))
|
|
self.reject(lambda d,l,p:p['events'][0].update(down=1))
|
|
self.reject(lambda d,l,p:p['events'][1].update(monotonic=10.4))
|
|
def test_final_coasting_or_wrong_waypoint_not_success(self):
|
|
self.reject(lambda d,l,p:d['snapshots'][35]['input'].update(velocity=[0,0,.1]))
|
|
self.reject(lambda d,l,p:d['snapshots'][34]['input'].update(velocity=[0,0,.1]))
|
|
self.reject(lambda d,l,p:d['snapshots'][35]['input'].update(origin=[6092.77,211.44,7156.4]))
|
|
def test_native_forward_input_required(self):
|
|
def mutation(d,l,p):
|
|
for s in d['snapshots'].values():s['input']['forward']=0
|
|
self.reject(mutation)
|
|
def test_gate_operation_or_teleport_refused(self):
|
|
self.reject(lambda d,l,p:d['snapshots'][27]['geometry'].update(door_state=.1))
|
|
self.reject(lambda d,l,p:d['snapshots'][27]['action'].update(pressed=1))
|
|
self.reject(lambda d,l,p:d['snapshots'][27]['input'].update(origin=[6092.77,211.44,7153.5]))
|
|
self.reject(lambda d,l,p:d['snapshots'][27].update(real_ms=5000))
|
|
def test_final_reference_and_unexpected_input_rejected(self):
|
|
self.reject(lambda d,l,p:p.update(final_sample=34))
|
|
self.reject(lambda d,l,p:d['snapshots'][27]['input'].update(input=[.2,0,1]))
|
|
def no_z(d,l,p):
|
|
for item in d['snapshots'].values():item['input']['input']=[0,0,0]
|
|
self.reject(no_z)
|
|
def test_native_clock_and_sample_references_bound(self):
|
|
self.reject(lambda d,l,p:d['ready'].update(real_ms=-40000))
|
|
self.reject(lambda d,l,p:p['stops'][0].update(before_sample=21))
|
|
self.reject(lambda d,l,p:p['samples'].append({'sample':35}))
|
|
self.reject(lambda d,l,p:p.update(deadline_monotonic=100.))
|
|
def test_helper_native_clocks_and_receipts_cannot_be_rewritten(self):
|
|
self.reject(lambda d,l,p:p['samples'][3].update(real_ms=99999))
|
|
self.reject(lambda d,l,p:p['samples'][3].update(received_monotonic=11.9))
|
|
self.reject(lambda d,l,p:p['events'][0].update(monotonic=10.1))
|
|
def test_unacknowledged_or_oversized_mouse_proposals_refused(self):
|
|
self.reject(lambda d,l,p:p['motions'].append({'dx':129,'dy':0,'monotonic':10.2,'after_sample':21,'response_sample':23}))
|
|
self.reject(lambda d,l,p:p['motions'].append({'dx':24,'dy':0,'monotonic':10.2,'after_sample':21,'response_sample':23}))
|
|
|
|
def test_coarse_profile_is_known_explicit_and_has_no_default_upgrade(self):
|
|
import math
|
|
def turn(d,l,p):
|
|
p['profile']='coarse'
|
|
angle=math.radians(155)
|
|
d['snapshots'][21]['look']['direction']=[math.sin(angle),0,math.cos(angle)]
|
|
p['motions']=[{'dx':300,'dy':0,'monotonic':10.1,'after_sample':21,'response_sample':23}]
|
|
self.assertTrue(self.run_case(turn)['verified'])
|
|
def fine(d,l,p):turn(d,l,p);p.pop('profile')
|
|
self.assertFalse(self.run_case(fine)['verified'])
|
|
for profile in ('unknown',True,None):self.reject(lambda d,l,p:p.update(profile=profile))
|
|
def overshoot(d,l,p):
|
|
turn(d,l,p);p['motions'][0]['response_sample']=24
|
|
angle=math.radians(191)
|
|
d['snapshots'][22]['look']['direction']=[math.sin(angle),0,math.cos(angle)]
|
|
self.reject(overshoot)
|
|
def excessive(d,l,p):turn(d,l,p);p['motions'][0]['dx']=385
|
|
self.reject(excessive)
|
|
|
|
def test_actual_coarse_policy_trace_passes_independent_raw_chain(self):
|
|
# Simulated dynamics exercise actual policy and every raw parser; no
|
|
# native runtime/input claim and no mocked evidence verdicts.
|
|
from test_prison_alignment_evidence import build, raw, ApproachScene
|
|
original = ApproachScene.run
|
|
with patch.object(ApproachScene, 'run', lambda scene: original(scene, profile='coarse')):
|
|
frames, look, route, _ = build()
|
|
self.assertEqual(route['profile'], 'coarse')
|
|
self.assertTrue(any(abs(m['dx']) > 128 for m in route['motions']))
|
|
result = v.validate_approach_evidence(raw(frames), 'test', look, route)
|
|
self.assertTrue(result['verified'], result)
|
|
route['profile'] = 'standard'
|
|
self.assertFalse(v.validate_approach_evidence(raw(frames), 'test', look, route)['verified'])
|
|
|
|
def test_coarse_does_not_expand_clocks_or_permit_action_keys(self):
|
|
for change in (lambda p:p.update(deadline_monotonic=100),lambda p:p['events'][1].update(monotonic=11),lambda p:p['events'][0].update(key='F')):
|
|
def mutate(d,l,p):p['profile']='coarse';change(p)
|
|
self.reject(mutate)
|
|
|
|
if __name__=='__main__':unittest.main()
|