Files
EscapistsReintroduced/tests/test_prison_action_selection_evidence.py
T

152 lines
9.6 KiB
Python

"""Actual policies -> complete synthetic native RAW -> independent validators.
No raw parser, prerequisite validator, display, or native API is mocked.
"""
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)]
from test_prison_alignment_evidence import build as alignment_fixture, raw as gate_raw
from test_prison_action_selection_proof import ChoiceScene
import prison_alignment_evidence as facing_validator
import prison_action_selection_evidence as validator
def build():
frames,look,route,unused=alignment_fixture()
frames={i:s for i,s in frames.items() if i<=route['final_sample']}
s=ChoiceScene();end=route['samples'][-1]
s.now=route['finished_monotonic'];s.next_sample=(end['real_ms']-2000)/1000+.1
s.geometry=copy.deepcopy(route['geometry']);s.origin=list(end['origin'])
s.camera=[s.origin[0],s.origin[1]+1.7,s.origin[2]-.1]
s.angle=math.degrees(math.atan2(end['direction'][0],end['direction'][2]));s.guard_cost=.18
make=s.make
def snapshot():
v=make();v['sample']=int(round((2000+s.now*1000)/100));v['real_ms']=2000+int(round(s.now*1000))
v['choices'].update(nonce='test',sample=v['sample'],real_ms=v['real_ms'])
return v
s.make=snapshot;v=snapshot();v.update(sample=end['sample'],real_ms=end['real_ms'],received_monotonic=end['received_monotonic'])
v['choices'].update(sample=v['sample'],real_ms=v['real_ms'])
s.snapshot=v;s.history=[copy.deepcopy(v)];s.last_read=v['sample']-1;s.pending=[]
s.calibration.update(nonce='test',ready_real_ms=2000,ready_monotonic=0.,observer_start_real_ms=0,
observer_deadline_monotonic=42.5,alignment_deadline_monotonic=s.now+8)
s.report={'approach_proof':route};s.accepted={'verified':True,'last_sample':route['final_sample']}
facing=s.facing();proof=s.select_action()
for frame in s.history:frames[frame['sample']]=copy.deepcopy(frame)
return frames,look,route,facing,proof
def raw(frames):
content=gate_raw(frames)
lines=[]
for i,s in sorted(frames.items()):
c=s.get('choices',{'selected_id':-1,'selected_class':'none','actions':[]})
a=c['actions'];entries=','.join(f'{n}={e["class"]}:{e["id"]}:{int(e["permitted"])}' for n,e in enumerate(a))
lines.append(f'ERR GATE CHOICE: run=test sample={i} real={s["real_ms"]} selected={c["selected_class"]}:{c["selected_id"]} context={int(s["context_selected"])} actions={len(a)} permissions={len(a)} shown={len(a)} truncated=0 ordered=[{entries}]')
return content+'\n'.join(lines)+'\n'
class SelectionEvidenceTests(unittest.TestCase):
@classmethod
def setUpClass(cls):cls.template=build()
def result(self,mutation=None):
frames,look,route,facing,proof=copy.deepcopy(self.template)
if mutation:mutation(frames,look,route,facing,proof)
return validator.validate_selection_evidence(raw(frames),'test',look,route,facing,proof)
def reject(self,mutation):
r=self.result(mutation);self.assertFalse(r['verified'],r);self.assertTrue(r['errors'])
def test_actual_policy_chain_passes_complete_unmocked_native_parsers(self):
f,l,a,p,s=copy.deepcopy(self.template)
r=facing_validator.validate_facing_evidence(raw(f),'test',l,a,p)
self.assertTrue(r['verified'],r);self.assertFalse(r['lock_selection_verified'])
r=self.result();self.assertTrue(r['verified'],r)
self.assertEqual(r['selected_action_id'],0);self.assertFalse(r['held_action_verified']);self.assertFalse(r['escape_verified'])
def test_prior_look_approach_or_facing_cannot_be_asserted(self):
for which in (1,2,3):
self.reject(lambda *args:args[which].update(completed=False))
def test_extra_or_wrong_wheel_and_unconfirmed_release_refuse(self):
self.reject(lambda f,l,a,p,s:s['wheel'].update(button=4))
self.reject(lambda f,l,a,p,s:s['wheel'].update(released=False))
self.reject(lambda f,l,a,p,s:s['wheel'].update(confirmed=False))
self.reject(lambda f,l,a,p,s:s.update(events=[{'key':'F','down':True}]))
self.reject(lambda f,l,a,p,s:s.update(motions=[{'dx':1,'dy':0}]))
self.reject(lambda f,l,a,p,s:s['wheel'].update(events=[4,5]))
self.reject(lambda f,l,a,p,s:s.update(wheel_events=[{'button':4}]))
def test_full_3d_body_drift_and_capture_context_refuse(self):
def drift(f,l,a,p,s):
i=s['initial_sample']+1;f[i]['origin'][0]+=.04;f[i]['origin'][1]+=.04
self.reject(drift)
self.reject(lambda f,l,a,p,s:f[s['initial_sample']+1].update(context_selected=False))
self.reject(lambda f,l,a,p,s:f[s['initial_sample']+1].update(phase=2))
def test_original_choice_ids_order_and_permission_cannot_change(self):
self.reject(lambda f,l,a,p,s:f[s['final_sample']]['choices']['actions'][1].update(id=8))
self.reject(lambda f,l,a,p,s:f[s['final_sample']]['choices']['actions'].reverse())
self.reject(lambda f,l,a,p,s:f[s['final_sample']]['choices']['actions'][0].update(permitted=True))
self.reject(lambda f,l,a,p,s:f[s['final_sample']]['choices'].update(selected_id=1))
self.reject(lambda f,l,a,p,s:f[s['final_sample']]['choices'].update(selected_class='ERR_PrisonDoorAction'))
def test_facing_pin_cannot_change_before_final_pair(self):
def change(f,l,a,p,s):
for i in range(p['initial_sample'],p['final_sample']):
if f[i]['context_selected']:
f[i]['choices']['actions'][1]['id']=8;return
raise AssertionError('simulation must expose early native context')
self.reject(change)
def test_facing_is_not_lock_selection_and_requires_context(self):
self.reject(lambda f,l,a,p,s:p.update(selection_samples=p['facing_samples']))
self.reject(lambda f,l,a,p,s:f[p['final_sample']].update(context_selected=False))
self.reject(lambda f,l,a,p,s:f[p['final_sample']].update(progress_bound=True))
def test_deadlines_hold_cap_and_ack_bound_are_not_renewable(self):
self.reject(lambda f,l,a,p,s:s.update(deadline_monotonic=s['deadline_monotonic']+.1))
self.reject(lambda f,l,a,p,s:s.update(finished_monotonic=s['deadline_monotonic']+.1))
self.reject(lambda f,l,a,p,s:s['wheel'].update(up_monotonic=s['wheel']['attempted_monotonic']+1.6))
self.reject(lambda f,l,a,p,s:s['wheel'].update(down_monotonic=s['started_monotonic']-.1))
self.reject(lambda f,l,a,p,s:s.update(finished_monotonic=s['wheel']['up_monotonic']+1.6))
def test_pre_dispatch_insufficient_remaining_time_refuses(self):
def exhausted(f,l,a,p,s):
# Jointly shortening a valid upstream lease must not bypass the
# selection policy's >=1.5s admission requirement.
d=s['wheel']['attempted_monotonic']+1.49
p['deadline_monotonic']=d;s['deadline_monotonic']=d
self.reject(exhausted)
def test_sample_receipts_stale_missing_or_nonfinite_refuse(self):
self.reject(lambda f,l,a,p,s:s['samples'][-1].update(received_monotonic=float('nan')))
self.reject(lambda f,l,a,p,s:s['samples'].pop(1))
self.reject(lambda f,l,a,p,s:s.update(finished_monotonic=s['samples'][-1]['received_monotonic']+.6))
self.reject(lambda f,l,a,p,s:s['wheel'].update(attempted_monotonic=s['started_monotonic']-.2))
def test_selected_lock_needs_consecutive_bound_neutral_samples(self):
self.reject(lambda f,l,a,p,s:f[s['selection_samples'][0]].update(progress_bound=False))
self.reject(lambda f,l,a,p,s:f[s['selection_samples'][0]].update(pressed=1))
self.reject(lambda f,l,a,p,s:s.update(selection_samples=[s['final_sample'],s['final_sample']]))
self.reject(lambda f,l,a,p,s:f[s['selection_samples'][0]].update(input=[0,0,1]))
def test_native_lock_selected_before_wheel_is_not_acknowledgement(self):
def early(f,l,a,p,s):
i=s['wheel']['after_sample'];f[i].update(selected=True,progress_bound=True)
f[i]['choices'].update(selected_id=0,selected_class='ERR_UnlockPrisonAction')
self.reject(early)
def test_selected_then_dropped_then_reacquired_cannot_hide_in_batch(self):
def dropped(f,l,a,p,s):
first=s['wheel']['after_sample']+1
f[first].update(selected=True,progress_bound=True);f[first]['choices'].update(selected_id=0,selected_class='ERR_UnlockPrisonAction')
f[first+1].update(selected=False,progress_bound=False);f[first+1]['choices'].update(selected_id=1,selected_class='ERR_PrisonDoorAction')
self.reject(dropped)
def test_final_bearing_and_stability_are_native(self):
def angle(f,l,a,p,s):
i=s['selection_samples'][0];v=f[i]['direction'];r=math.atan2(v[0],v[2])+math.radians(.3)
f[i]['direction']=[math.sin(r),0,math.cos(r)]
self.reject(angle)
self.reject(lambda f,l,a,p,s:f[s['final_sample']].update(direction=[0,0,1]))
def test_report_geometry_cannot_replace_native_measurement(self):
def forged(f,l,a,p,s):
p['geometry']['context'][0]+=.2;s['geometry']['context'][0]+=.2
self.reject(forged)
def test_missing_choice_record_or_reversed_choice_clock_refuses(self):
f,l,a,p,s=copy.deepcopy(self.template);content=raw(f)
line=next(x for x in content.splitlines() if x.startswith(f'ERR GATE CHOICE: run=test sample={s["final_sample"]} '))
self.assertFalse(validator.validate_selection_evidence(content.replace(line+'\n',''),'test',l,a,p,s)['verified'])
self.assertFalse(validator.validate_selection_evidence(content.replace(line,line.replace('run=test','run=other')),'test',l,a,p,s)['verified'])
if __name__=='__main__':unittest.main()