Files
EscapistsReintroduced/tests/test_prison_alignment_evidence.py
T

153 lines
10 KiB
Python

"""Independent alignment validation with complete synthetic native-format records."""
import copy
import math
from pathlib import Path
import sys
import unittest
sys.path[:0]=[str(Path(__file__).resolve().parents[1]/'tools'),str(Path(__file__).resolve().parent)]
import prison_alignment_evidence as validator
import prison_approach_evidence as route_validator
import prison_alignment_proof as alignment
from test_prison_approach_proof import Scene as ApproachScene
from test_prison_alignment_proof import Scene as AlignmentScene
def build():
route_scene=ApproachScene();route=route_scene.run()
scene=AlignmentScene()
scene.now=route_scene.now;scene.next_sample=route_scene.next_sample
scene.geometry=copy.deepcopy(route_scene.geometry)
scene.origin=list(route_scene.position);scene.camera=[scene.origin[0],scene.origin[1]+1.7,scene.origin[2]-.1]
scene.angle=route_scene.angle;scene.guard_cost=.18;scene.pending=[]
make=scene.make
def snapshot():
value=make();value['sample']=int(round((2000+scene.now*1000)/100));value['real_ms']=2000+int(round(scene.now*1000))
wanted=math.degrees(math.atan2(scene.geometry['context'][0]-scene.camera[0],scene.geometry['context'][2]-scene.camera[2]))
chosen=abs((wanted-scene.angle+180)%360-180)<=3
for key in ('selected','context_selected','cached_performable','progress_bound'):value[key]=chosen
return value
scene.make=snapshot
scene.prior=copy.deepcopy(route['samples'][-1])
value=snapshot();value['sample']=scene.prior['sample'];value['real_ms']=scene.prior['real_ms'];value['received_monotonic']=scene.prior['received_monotonic']
scene.snapshot=value;scene.history=[copy.deepcopy(value)];scene.last_read=value['sample']-1
scene.calibration.update(nonce='test',ready_real_ms=2000,ready_monotonic=0.,observer_start_real_ms=0,
observer_deadline_monotonic=42.5,alignment_deadline_monotonic=scene.now+8)
scene.report={'approach_proof':route};scene.accepted={'verified':True,'last_sample':route['final_sample']}
proof=scene.run()
look={'requested':True,'completed':True,'started_monotonic':0.,'finished_monotonic':.65,
'baseline_samples':[21,22],'response_sample':24,'return_sample':26,
'motions':[{'dx':24,'dy':0,'monotonic':.25,'after_sample':22,'after_real_ms':2200},
{'dx':-24,'dy':0,'monotonic':.45,'after_sample':24,'after_real_ms':2400}]}
frames={}
for i in range(1,46):
degrees=24*.085 if i in (24,25) else 0.
radians=math.radians(degrees)
frames[i]={'sample':i,'real_ms':100*i,'origin':[98.5,0.,98.5],
'direction':[math.sin(radians),0.,math.cos(radians)],'body_forward':[0.,0.,1.],
'camera':[98.5,1.7,98.4],'input':[0.,0.,0.],'velocity':[0.,0.,0.],
'forward':0.,'inventory':False,'phase':1,'geometry':copy.deepcopy(route_scene.geometry),
'selected':False,'context_selected':False,'cached_performable':False,'progress_bound':False,
'performing':False,'progress':0.,'pressed':0.,'duration':4.,'door_state':0.,'door_control':0.}
for s in route_scene.history:
value=copy.deepcopy(s)
value.update(camera=[s['origin'][0],s['origin'][1]+1.7,s['origin'][2]-.1],
selected=False,context_selected=False,cached_performable=False,progress_bound=False,
performing=False,progress=0.,pressed=0.,duration=4.)
frames[s['sample']]=value
for s in scene.history:frames[s['sample']]=copy.deepcopy(s)
return frames,look,route,proof
def raw(frames):
vector=lambda v:'<'+','.join(format(x,'.10g') for x in v)+'>'
lines=['ERR INPUT OBSERVE: run=test sample=0 real=0 world=0 QUEUED']
for i,s in sorted(frames.items()):
stamp=f'run=test sample={i} real={s["real_ms"]}'
lines.append(f'ERR INPUT OBSERVE: {stamp} world={s["real_ms"]} SAMPLE origin={vector(s["origin"])} forward={s["forward"]} input={vector(s["input"])} velocity={vector(s["velocity"])} inventory={int(s["inventory"])}')
if i==1:lines.append(f'ERR LOOK OBSERVE: {stamp} BASIS native-player-camera-world-transform')
lines.append(f'ERR LOOK OBSERVE: {stamp} SAMPLE direction={vector(s["direction"])} camera={vector(s["camera"])} body={vector(s["origin"])} bodyForward={vector(s["body_forward"])} turn=<0,0,0>')
if i==1:lines.append(f'ERR GATE OBSERVE: {stamp} BASIS registered-prison-lock-native-interaction')
lines.append(f'ERR GATE OBSERVE: {stamp} SAMPLE phase={s["phase"]} selected={int(s["selected"])} context={int(s["context_selected"])} cachedPerformable={int(s["cached_performable"])} progressBound={int(s["progress_bound"])} performing={int(s["performing"])} progress={s["progress"]} duration={s["duration"]} pressed={s["pressed"]}')
g=s['geometry']
lines.append(f'ERR GATE OBSERVE: {stamp} GEOMETRY body={vector(s["origin"])} context={vector(g["context"])} radius={g["radius"]} doorState={s["door_state"]} doorControl={s["door_control"]} prison={vector(g["prison"])} gate={vector(g["gate"])} gateForward={vector(g["gate_forward"])}')
if i==20:lines.append('ERR INPUT OBSERVE: run=test sample=20 real=2000 world=2000 READY')
return '\n'.join(lines)+'\n'
class AlignmentEvidenceTests(unittest.TestCase):
@classmethod
def setUpClass(cls):cls.template=build()
def result(self,mutate=None):
f,l,a,p=copy.deepcopy(self.template)
if mutate:mutate(f,l,a,p)
return validator.validate_alignment_evidence(raw(f),'test',l,a,p)
def reject(self,mutate):
result=self.result(mutate);self.assertFalse(result['verified'],result);self.assertTrue(result['errors'])
def test_actual_approach_and_alignment_modules_pass_unmocked_raw_validators(self):
f,l,a,p=copy.deepcopy(self.template)
prior=route_validator.validate_approach_evidence(raw(f),'test',l,a)
self.assertTrue(prior['verified'],prior)
result=self.result();self.assertTrue(result['verified'],result)
self.assertFalse(result['held_action_verified']);self.assertFalse(result['escape_verified'])
def test_prior_approach_and_calibration_cannot_be_claimed(self):
self.reject(lambda f,l,a,p:a.update(completed=False))
self.reject(lambda f,l,a,p:l.update(completed=False))
self.reject(lambda f,l,a,p:p.update(completed=False))
def test_historical_first_receipt_is_not_rewritten_to_stage_start(self):
f,l,a,p=copy.deepcopy(self.template)
p['started_monotonic']=p['samples'][0]['received_monotonic']+.6
# Still before the first motion; original historical receipts preserved.
result=validator.validate_alignment_evidence(raw(f),'test',l,a,p)
self.assertTrue(result['verified'],result)
def test_full_3d_drift_refused_even_below_each_axis_threshold(self):
def mutation(f,l,a,p):
i=p['initial_sample']+1;f[i]['origin'][0]+=.04;f[i]['origin'][1]+=.04
self.reject(mutation)
def test_action_selection_requires_exact_bound_stable_native_pair(self):
for field,value in [('selected',False),('context_selected',False),('cached_performable',False),('progress_bound',False)]:
with self.subTest(field=field):self.reject(lambda f,l,a,p:f[p['selection_samples'][-1]].update({field:value}))
self.reject(lambda f,l,a,p:p.update(selection_samples=[p['selection_samples'][0],p['selection_samples'][0]]))
def test_any_intermediate_gate_input_or_body_motion_refused(self):
for field,value in [('pressed',1.),('progress',.1),('performing',True),('door_state',.1),('input',[0,0,1])]:
with self.subTest(field=field):self.reject(lambda f,l,a,p:f[a['final_sample']+1].update({field:value}))
def test_deadline_and_original_lifetime_cannot_be_extended(self):
self.reject(lambda f,l,a,p:p.update(deadline_monotonic=p['started_monotonic']+8.1))
self.reject(lambda f,l,a,p:p.update(finished_monotonic=p['deadline_monotonic']+.1))
self.reject(lambda f,l,a,p:a.update(deadline_monotonic=10.))
def test_mouse_axis_caps_gain_sign_and_receipts_are_authoritative(self):
self.reject(lambda f,l,a,p:p['motions'][0].update(dy=1))
self.reject(lambda f,l,a,p:p['motions'][0].update(dx=129))
self.reject(lambda f,l,a,p:p['motions'][0].update(dx=-p['motions'][0]['dx']))
self.reject(lambda f,l,a,p:p['motions'][0].update(monotonic=p['started_monotonic']-.1))
self.reject(lambda f,l,a,p:p['samples'][-1].update(received_monotonic=float('nan')))
def test_intermediate_turn_overshoot_not_hidden_by_later_good_pose(self):
def mutation(f,l,a,p):
m=p['motions'][0];s=f[m['after_sample']+1]
baseline=math.degrees(math.atan2(f[m['after_sample']]['direction'][0],f[m['after_sample']]['direction'][2]))
angle=math.radians(baseline+20);s['direction']=[math.sin(angle),0.,math.cos(angle)]
self.reject(mutation)
def test_no_keyboard_events_or_stale_completion_can_be_smuggled(self):
self.reject(lambda f,l,a,p:p.update(events=[{'key':'F','down':True}]))
self.reject(lambda f,l,a,p:p.update(finished_monotonic=p['samples'][-1]['received_monotonic']+.6))
def test_unsettled_response_or_too_late_acknowledgement_refused(self):
def change(f,l,a,p):
m=p['motions'][0];i=m['response_sample']-1
direction=f[m['response_sample']]['direction'];angle=math.atan2(direction[0],direction[2])+math.radians(.3)
f[i]['direction']=[math.sin(angle),0,math.cos(angle)]
self.reject(change)
self.reject(lambda f,l,a,p:p['motions'][0].update(response_sample=p['final_sample']))
def test_wrong_final_bearing_and_original_queued_bound_refused(self):
def wrong(f,l,a,p):
for i in p['selection_samples']:f[i]['direction']=[0,0,1]
self.reject(wrong)
f,l,a,p=copy.deepcopy(self.template)
content=raw(f).replace('sample=0 real=0 world=0 QUEUED','sample=0 real=1000 world=0 QUEUED')
self.assertFalse(validator.validate_alignment_evidence(content,'test',l,a,p)['verified'])
def test_missing_raw_sample_and_changed_geometry_refused(self):
self.reject(lambda f,l,a,p:f.pop(p['initial_sample']+1))
self.reject(lambda f,l,a,p:f[p['initial_sample']+1]['geometry']['context'].__setitem__(0,999))
if __name__=='__main__':unittest.main()