"""Actual dormant held policy on simulated native snapshots; no input/runtime.""" import copy import math from pathlib import Path import sys import unittest sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools')) import prison_held_action_proof as policy class Scene: def __init__(self): self.now=30.;self.next_sample=30.1;self.guard_cost=.02 self.geometry={'nonce':'N','basis':'registered-prison-lock-native-interaction','prison':[100.,0.,100.], 'gate':[100.,0.,94.],'gate_forward':[0.,0.,1.],'context':[102.7,1.2,94.],'radius':2.} self.origin=[102.7,0.,95.5];self.camera=[102.7,1.7,95.4];self.direction=[0.,0.,-1.] self.actions=[{'class':'ERR_PrisonDoorAction','id':8,'permitted':False},{'class':'ERR_UnlockPrisonAction','id':3,'permitted':True}] self.focus=True;self.native_held=False;self.down=None;self.up=None;self.events=[];self.notifications=[] self.fail_down=False;self.fail_up=False;self.unknown_down=False;self.release_late=False;self.native_release_missing=False self.phase_delay=4.1;self.motion_delay=.15;self.never_complete=False;self.no_motion=False;self.no_control=False self.mutate=None;self.stale=False;self.history=[];self.last_read=299 self.snapshot=self.make();self.history=[copy.deepcopy(self.snapshot)] prior=copy.deepcopy(self.snapshot);prior.update(sample=299,real_ms=59900,received_monotonic=29.9);prior['choices'].update(sample=299,real_ms=59900) self.report={'action_selection_proof':{'completed':True,'nonce':'N','geometry':copy.deepcopy(self.geometry), 'final_sample':300,'selection_samples':[299,300],'samples':[prior,copy.deepcopy(self.snapshot)],'actions':copy.deepcopy(self.actions), 'deadline_monotonic':30.,'finished_monotonic':30.}} self.calibration={'nonce':'N','observer_start_real_ms':30000,'ready_real_ms':32000,'ready_monotonic':2.,'observer_deadline_monotonic':44.5} self.evidence={'verified':True,'last_sample':300,'selected_action_id':3,'wheel_button':5} def clock(self):return self.now def make(self): elapsed=None if self.down is None else self.now-self.down phase=2 if elapsed is not None and elapsed>=self.phase_delay and not self.never_complete else 1 performing=phase==1 and elapsed is not None and elapsed>=.1 and self.native_held progress=min(3.98,max(0,elapsed-.1)) if performing else 0. pressed=1. if self.native_held or (self.native_release_missing and self.down is not None) else 0. geo=copy.deepcopy(self.geometry) if phase==2:geo['context']=[102.,1.2,94.7];geo['gate_forward']=[.3,0.,.953939] selected=phase==1 s={'sample':int(round(self.now*10)),'real_ms':30000+int(round(self.now*1000)), 'received_monotonic':self.now-(1 if self.stale else 0),'geometry':geo,'phase':phase, 'origin':list(self.origin),'camera':list(self.camera),'direction':list(self.direction), 'body_forward':[0.,0.,-1.],'input':[0.,0.,0.],'forward':0.,'velocity':[0.,0.,0.],'turn':[0.,0.,0.], 'inventory':False,'door_state':.25 if phase==2 and elapsed>=self.phase_delay+self.motion_delay and not self.no_motion else 0., 'door_control':1. if phase==2 and not self.no_control else 0.,'selected':selected,'context_selected':selected, 'cached_performable':selected,'progress_bound':selected,'performing':performing,'progress':progress,'pressed':pressed,'duration':4.} s['choices']={'nonce':'N','sample':s['sample'],'real_ms':s['real_ms'],'selected_id':3 if selected else -1, 'selected_class':'ERR_UnlockPrisonAction' if selected else 'none','actions':copy.deepcopy(self.actions) if selected else []} if self.mutate:self.mutate(s,elapsed) return s def sleep(self,seconds): end=self.now+seconds while self.now=self.next_sample-1e-8: self.snapshot=self.make();self.history.append(copy.deepcopy(self.snapshot));self.next_sample+=.1 def check(self): if not self.focus:raise policy.Refused('original private owner lost') self.sleep(self.guard_cost) def idle(self): if self.native_held:raise policy.Refused('native keys not idle') def observe(self): batch=[x for x in self.history if x['sample']>self.last_read] or [self.snapshot] self.last_read=batch[-1]['sample'];return copy.deepcopy(batch) def down_f(self,guard,*,deadline,not_after): guard.check();self.idle() if self.now>not_after or self.now>=deadline:raise policy.Refused('adapter admission expired') self.native_held=True;self.down=self.now;self.events.append(('F',True,self.now,deadline)) if self.unknown_down:raise policy.Refused('down request outcome uncertain') if self.fail_down:raise policy.Refused('down refused') return {'key':'F','down_monotonic':self.now} def up_f(self,guard,*,deadline,cleanup=False): guard.check() if self.fail_up:raise policy.Refused('up refused') if self.release_late and not cleanup:self.sleep(2.) self.native_held=False;self.up=self.now;self.events.append(('F',False,self.now,deadline)) return {'key':'F','up_monotonic':self.now,'released':True} def run(self): return policy.perform_held_action(self,self.idle,self,self.observe,self.geometry,self.calibration,self.evidence,self.report, clock=self.clock,sleep=self.sleep,notify=self.notifications.append) class HeldTests(unittest.TestCase): def test_one_real_policy_hold_progress_reset_transition_motion_and_release(self): s=Scene();r=s.run() self.assertTrue(r['completed']);self.assertEqual(r['status'],'verified') self.assertEqual([(e[0],e[1]) for e in s.events],[('F',True),('F',False)]) self.assertGreaterEqual(len(r['progress_samples']),2);self.assertTrue(r['unlock_observed'] and r['gate_motion_observed']) self.assertTrue(r['release_confirmed']);self.assertIsNone(r['unconfirmed_held_key']) self.assertEqual(r['paired_release_errors'],[]);self.assertLess(r['released_sample'],451) self.assertGreater(r['released_sample'],r['release_barrier_sample']);self.assertLess(s.now,44.5) self.assertEqual(r['original_deadline_monotonic'],44.5) self.assertLessEqual(r['held_deadline_monotonic']-r['press_attempted_monotonic'],5) self.assertLess(r['up_monotonic'],r['held_deadline_monotonic']) frames={x['sample']:x for x in r['samples']} self.assertTrue(frames[r['transition_sample']]['progress']==0 and not frames[r['transition_sample']]['selected']) self.assertNotEqual(frames[r['motion_sample']]['geometry']['context'],s.geometry['context']) def test_does_not_renew_previous_selection_eight_second_lease(self): s=Scene();self.assertEqual(s.report['action_selection_proof']['deadline_monotonic'],30.) r=s.run();self.assertTrue(r['completed']);self.assertEqual(r['original_deadline_monotonic'],44.5) self.assertEqual(s.report['action_selection_proof']['deadline_monotonic'],30.) def test_insufficient_original_budget_sends_nothing(self): s=Scene();s.calibration['observer_deadline_monotonic']=35.9 with self.assertRaisesRegex(ValueError,'insufficient-original-budget'):s.run() self.assertEqual(s.events,[]);self.assertEqual(s.report['held_action_proof']['status'],'refused-no-input') def test_no_verified_selection_no_press(self): for fault in ('verdict','nonce','action','pair','deadline'): s=Scene() if fault=='verdict':s.evidence['verified']=False if fault=='nonce':s.calibration['nonce']='foreign' if fault=='action':s.evidence['selected_action_id']=8 if fault=='pair':s.report['action_selection_proof']['selection_samples']=[298,300] if fault=='deadline':s.calibration['observer_deadline_monotonic']=60 with self.subTest(fault=fault),self.assertRaises(ValueError):s.run() self.assertEqual(s.events,[]) def test_early_reset_or_missing_native_input_refuses_and_releases(self): for fault in ('reset','pressed','performing','decrease','unbound','selection','permission'): s=Scene() def mutate(x,e): if e is None or e<1 or x['phase']!=1:return if fault=='reset':x['progress']=0 if fault=='pressed':x['pressed']=0 if fault=='performing':x['performing']=False if fault=='decrease':x['progress']=.01 if fault=='unbound':x['progress_bound']=False if fault=='selection':x['selected']=False if fault=='permission':x['cached_performable']=False s.mutate=mutate with self.subTest(fault=fault),self.assertRaises(ValueError):s.run() self.assertEqual([e[1] for e in s.events],[True,False]);self.assertFalse(s.native_held) self.assertFalse(s.report['held_action_proof']['completed']) def test_phase_without_advancing_progress_is_not_completion(self): s=Scene();s.phase_delay=.1 with self.assertRaisesRegex(ValueError,'advancing bound progress'):s.run() self.assertEqual([e[1] for e in s.events],[True,False]) def test_intermediate_identity_geometry_body_input_phase_refusals(self): for fault in ('phase0','phase3','body','turn','input','menu','geometry','gap','nonce'): s=Scene() def mutate(x,e): if e is None or not .84.3:x['phase']=1 s.mutate=mutate with self.assertRaisesRegex(ValueError,'phase reversed'):s.run() self.assertTrue(s.report['held_action_proof']['unlock_observed']);self.assertFalse(s.report['held_action_proof']['completed']) def test_unlock_without_motion_or_control_is_not_full_proof(self): for fault in ('no_motion','no_control'): s=Scene();setattr(s,fault,True) with self.subTest(fault=fault),self.assertRaises(ValueError):s.run() r=s.report['held_action_proof'];self.assertTrue(r['unlock_observed']);self.assertFalse(r['completed']) self.assertEqual(r['status'],'unlock-observed-motion-unverified');self.assertFalse(s.native_held) def test_renewed_native_input_after_acknowledgement_refuses_before_delayed_motion(self): for buffered in (False,True): s=Scene();s.motion_delay=.8 def mutate(x,e): if e is not None and 4.350:x['progress']=.5 s.mutate=mutate with self.assertRaisesRegex(ValueError,'advancing bound progress'):s.run() self.assertFalse(s.report['held_action_proof']['unlock_observed']) self.assertEqual([x[1] for x in s.events],[True,False]) def test_first_post_release_buffered_reset_is_not_release_acknowledgement(self): s=Scene();s.no_motion=True original=s.observe def observe(): batch=original() if s.up is not None: # Freeze the first post-release batch forever: all are reset, # but none is newer than the full drained acknowledgement barrier. if not hasattr(s,'frozen_after_up'):s.frozen_after_up=copy.deepcopy(batch) return copy.deepcopy(s.frozen_after_up[-1:]) if s.last_read>s.frozen_after_up[-1]['sample'] else batch return batch s.observe=observe with self.assertRaises(ValueError):s.run() r=s.report['held_action_proof'];self.assertTrue(r['release_confirmed']);self.assertIsNone(r['released_sample']) def test_primary_failure_survives_notification_cleanup_failure(self): s=Scene();s.unknown_down=True def notify(event): s.notifications.append(event) if event['kind']=='held-action-finished':raise policy.Refused('supervisor pipe closed') with self.assertRaisesRegex(ValueError,'down request outcome uncertain'): policy.perform_held_action(s,s.idle,s,s.observe,s.geometry,s.calibration,s.evidence,s.report, clock=s.clock,sleep=s.sleep,notify=notify) r=s.report['held_action_proof'];self.assertEqual(r['error'],'down request outcome uncertain') self.assertEqual(r['notification_errors'],['supervisor pipe closed']) self.assertEqual([x[1] for x in s.events],[True,False]) def test_retry_cannot_press_again(self): s=Scene();s.run();count=len(s.events) with self.assertRaisesRegex(ValueError,'already exists'):s.run() self.assertEqual(len(s.events),count) def test_stale_snapshot_and_old_initial_phase_refuse_without_input(self): for fault in ('stale','phase'): s=Scene() if fault=='stale':s.stale=True;s.sleep(.1) else:s.history[0]['phase']=2 with self.subTest(fault=fault),self.assertRaises(ValueError):s.run() self.assertEqual(s.events,[]) if __name__=='__main__':unittest.main()