Qualify route gain and observe native gate context through ordinary input
This commit is contained in:
@@ -718,6 +718,8 @@ class ApproachTests(unittest.TestCase):
|
||||
report={'approach_requested':True,'approach_proof':{'samples':full,'motions':[{'dx':128,'dy':0,'monotonic':10,'after_sample':i,'response_sample':i+1} for i in range(16)],'events':[{'key':'W','down':bool(i%2),'monotonic':10} for i in range(24)]},
|
||||
'look_observation':observer.look_records,'gate_observation':observer.gate_records,
|
||||
'observer':{'records':observer.raw,'events':observer.events},'provenance_budget':'x'*65536}
|
||||
report['alignment_requested']=True
|
||||
report['alignment_proof']={'samples':full[-80:],'motions':[{'dx':128,'dy':0,'monotonic':10,'after_sample':i,'response_sample':i+1} for i in range(6)]}
|
||||
payload=json.dumps(proof.report_for_write(report),indent=2).encode()
|
||||
self.assertEqual(len(observer.look_records),451);self.assertEqual(len(observer.gate_records),901)
|
||||
self.assertLess(len(payload),1024*1024)
|
||||
@@ -749,4 +751,173 @@ class ApproachTests(unittest.TestCase):
|
||||
stop.assert_called_once();self.assertIsNone(failed.call_args.args[1])
|
||||
|
||||
|
||||
|
||||
class AlignmentIntegrationTests(unittest.TestCase):
|
||||
def test_cli_requires_explicit_approach_and_look(self):
|
||||
base=['--enable-private-keyboard-proof','--prefix','/tmp/pfx','--game-executable','/tmp/game',
|
||||
'--profile','/tmp/profile','--report','/tmp/report','--host-display',':0','--nonce','N']
|
||||
for flags in ([],['--enable-private-look-proof'],['--enable-private-approach-proof']):
|
||||
with self.assertRaises(SystemExit),patch('sys.stderr'):
|
||||
proof.arguments(base+flags+['--enable-private-alignment-proof'])
|
||||
args=proof.arguments(base+['--enable-private-look-proof','--enable-private-approach-proof','--enable-private-alignment-proof'])
|
||||
self.assertTrue(args.enable_private_alignment_proof)
|
||||
self.assertFalse(proof.arguments(base).enable_private_alignment_proof)
|
||||
def test_alignment_follows_completed_approach_without_more_keyboard_events(self):
|
||||
clock=Clock();o=FakeObserver();o.enable_approach=True;o.ready_at=0;k=FakeKeyboard(o);r={};order=[];notices=[]
|
||||
def look(*a):order.append('look')
|
||||
def approach(g,k,o,m,r,c,s):
|
||||
order.append('approach');k.send('W',True);k.send('W',False)
|
||||
r['approach_proof']={'completed':True,'final_sample':99,'samples':[{'sample':99}], 'paired_release_errors':[],'unconfirmed_held_key':None}
|
||||
def alignment(g,k,o,m,r,c,s):
|
||||
self.assertEqual(order,['look','approach']);self.assertTrue(r['approach_proof']['completed'])
|
||||
self.assertEqual(notices[-1],{'kind':'approach-finished','successful':True})
|
||||
order.append('alignment');r['alignment_proof']={'completed':True}
|
||||
with patch.object(proof,'perform_look',side_effect=look),patch.object(proof,'run_approach',side_effect=approach),patch.object(proof,'run_alignment',side_effect=alignment),patch.object(proof,'notify',side_effect=notices.append):
|
||||
proof.sequence(FakeGuard(),k,o,r,clock,clock.sleep,mouse=object(),approach=True,alignment=True)
|
||||
self.assertEqual(order,['look','approach','alignment'])
|
||||
self.assertEqual(k.events,[('W',True),('W',False)]);self.assertTrue(r['sequence_completed'])
|
||||
self.assertEqual(r['approach_proof']['samples'],[{'sample':99}])
|
||||
self.assertEqual(r['approach_proof']['final_sample'],99)
|
||||
def test_failed_alignment_does_not_erase_approach_or_reactivate_its_lease(self):
|
||||
clock=Clock();o=FakeObserver();o.enable_approach=True;o.ready_at=0;k=FakeKeyboard(o);r={};notices=[]
|
||||
def approach(*args):r['approach_proof']={'completed':True,'final_sample':100,'paired_release_errors':[],'unconfirmed_held_key':None}
|
||||
with patch.object(proof,'perform_look'),patch.object(proof,'run_approach',side_effect=approach),patch.object(proof,'run_alignment',side_effect=proof.Refused('selection missing')),patch.object(proof,'notify',side_effect=notices.append):
|
||||
with self.assertRaisesRegex(proof.Refused,'selection missing'):
|
||||
proof.sequence(FakeGuard(),k,o,r,clock,clock.sleep,mouse=object(),approach=True,alignment=True)
|
||||
self.assertFalse(r['sequence_completed']);self.assertTrue(r['approach_proof']['completed'])
|
||||
self.assertTrue(all(e['successful'] for e in notices if e['kind']=='approach-finished'))
|
||||
self.assertEqual(k.events,[])
|
||||
def test_yaw_only_capability_checks_optin_bounds_original_deadline_and_native_guard(self):
|
||||
class Fn:
|
||||
def __init__(self,fn):self.fn=fn
|
||||
def __call__(self,*args):return self.fn(*args)
|
||||
events=[];xt=SimpleNamespace(XTestFakeRelativeMotionEvent=Fn(lambda *a:events.append(a) or 1))
|
||||
query=SimpleNamespace(x=SimpleNamespace(XQueryPointer=Fn(lambda *a:1),XFlush=lambda d:None),d=123)
|
||||
mouse=proof.RelativeMouse(query,SimpleNamespace(xt=xt,require_idle=lambda:None),{'window':100,'pid':9},enable_approach=True)
|
||||
guard=FakeGuard();clock=Clock();mouse.validate_pointer=lambda *a:None
|
||||
with self.assertRaises(proof.Refused):mouse.alignment_move(24,0,guard,deadline=8,clock=clock)
|
||||
mouse.enable_alignment=True
|
||||
for dx in (1,-128,128):mouse.alignment_move(dx,0,guard,deadline=8,clock=clock)
|
||||
for dx,dy in ((0,0),(129,0),(-129,0),(1,1),(True,0),(1,False)):
|
||||
with self.assertRaises(proof.Refused):mouse.alignment_move(dx,dy,guard,deadline=8,clock=clock)
|
||||
wrapped=proof.AlignmentMouse(mouse,{},clock,8)
|
||||
with self.assertRaisesRegex(proof.Refused,'extend'):wrapped.move(1,0,guard,deadline=8.001)
|
||||
def late(*a):clock.now=8
|
||||
mouse.validate_pointer=late
|
||||
with self.assertRaisesRegex(proof.Refused,'deadline'):wrapped.move(1,0,guard,deadline=8)
|
||||
self.assertEqual(len(events),3)
|
||||
def test_snapshot_has_native_camera_action_fields_and_independent_alignment_cursor(self):
|
||||
from test_ordinary_gate_evidence import fixture,NONCE
|
||||
clean='\n'.join(s for s in fixture().splitlines() if 'CANCELLED' not in s)+'\n'
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path=Path(folder)/'console.log';path.write_text('')
|
||||
o=proof.Observer(path,NONCE,clock=lambda:10.,enable_look=True,enable_approach=True)
|
||||
try:
|
||||
with path.open('a') as out:out.write(clean)
|
||||
first=o.approach_snapshots()[-1];self.assertEqual(first['sample'],30)
|
||||
self.assertEqual(first['camera'],(6088.58,213.019,7158.56))
|
||||
for key in ('selected','context_selected','cached_performable','progress_bound','performing'):self.assertIs(first[key],False)
|
||||
self.assertEqual((first['pressed'],first['progress'],first['duration']),(0,0,4))
|
||||
o.alignment_previous=27
|
||||
self.assertEqual([s['sample'] for s in o.alignment_snapshots()],[28,29,30])
|
||||
self.assertEqual(o.approach_previous,30);self.assertEqual(o.alignment_previous,30)
|
||||
self.assertEqual(o.approach_snapshots(),[first])
|
||||
finally:o.close()
|
||||
def test_failed_independent_approach_blocks_alignment_module(self):
|
||||
o=SimpleNamespace(ready_at=0,ready_real=3000,queued_real=1000,nonce='N',approach_data=lambda:('raw',{}))
|
||||
with patch('prison_approach_evidence.validate_approach_evidence',return_value={'verified':False,'errors':['raw route mismatch']}),patch('prison_alignment_proof.perform_alignment') as module:
|
||||
with self.assertRaisesRegex(proof.Refused,'prerequisite refused'):
|
||||
proof.run_alignment(None,None,o,None,{'look_proof':{}},lambda:30,lambda s:None)
|
||||
module.assert_not_called()
|
||||
def test_alignment_adapter_passes_readonly_keyboard_and_original_lease(self):
|
||||
import copy
|
||||
clock=Clock();clock.now=34
|
||||
route={'completed':True,'final_sample':100,'geometry':{'nonce':'N'},'samples':[{'sample':100,'origin':[1,2,3]}]}
|
||||
original=copy.deepcopy(route);report={'approach_proof':route,'look_proof':{'baseline_samples':[20,21],'response_sample':22}}
|
||||
o=SimpleNamespace(ready_at=0,ready_real=3000,queued_real=1000,nonce='N',approach_data=lambda:('raw',{}),
|
||||
look_records=[{'kind':'sample','sample':21,'direction':[0,0,1]},{'kind':'sample','sample':22,'direction':[.03,0,.999]}],
|
||||
alignment_snapshots=lambda:[{'sample':100}],alignment_previous=None)
|
||||
keyboard=SimpleNamespace(require_idle=lambda:None);motions=[];notices=[]
|
||||
mouse=SimpleNamespace(alignment_move=lambda *a,**k:motions.append((a,k)))
|
||||
def module(guard,require_idle,m,observe,geometry,calibration,evidence,r,**kwargs):
|
||||
self.assertTrue(callable(require_idle));self.assertFalse(hasattr(require_idle,'send'))
|
||||
self.assertEqual(o.alignment_previous,99)
|
||||
self.assertEqual(calibration['observer_deadline_monotonic'],42.5)
|
||||
self.assertEqual(calibration['alignment_deadline_monotonic'],42)
|
||||
m.move(12,0,guard,deadline=42)
|
||||
r['alignment_proof']={'completed':True,'samples':[{'sample':100}]}
|
||||
kwargs['notify']({'kind':'alignment-pending','deadline_monotonic':42})
|
||||
kwargs['notify']({'kind':'alignment-finished','successful':True})
|
||||
with patch('prison_approach_evidence.validate_approach_evidence',return_value={'verified':True,'last_sample':100}),patch('prison_alignment_proof.perform_alignment',side_effect=module),patch.object(proof,'notify',side_effect=notices.append):
|
||||
proof.run_alignment(FakeGuard(),keyboard,o,mouse,report,clock,clock.sleep)
|
||||
self.assertEqual(route,original);self.assertEqual(motions[0][1]['deadline'],42)
|
||||
self.assertEqual([n['kind'] for n in notices],['alignment-pending','alignment-pending','alignment-finished'])
|
||||
self.assertTrue(notices[-1]['successful'])
|
||||
# A later start has only the original remaining lifetime, never a new 8s.
|
||||
clock.now=40;notices.clear();report.pop('alignment_proof')
|
||||
def late_module(g,k,m,observe,geometry,calibration,evidence,r,**kwargs):
|
||||
self.assertEqual(calibration['alignment_deadline_monotonic'],42.5)
|
||||
clock.now=42.5;r['alignment_proof']={'completed':True}
|
||||
with patch('prison_approach_evidence.validate_approach_evidence',return_value={'verified':True,'last_sample':100}),patch('prison_alignment_proof.perform_alignment',side_effect=late_module),patch.object(proof,'notify',side_effect=notices.append):
|
||||
with self.assertRaisesRegex(proof.Refused,'deadline'):proof.run_alignment(FakeGuard(),keyboard,o,mouse,report,clock,clock.sleep)
|
||||
self.assertFalse(notices[-1]['successful'])
|
||||
def test_actual_alignment_module_runs_through_helper_capabilities(self):
|
||||
import copy
|
||||
from test_prison_alignment_proof import Scene
|
||||
scene=Scene();report=scene.report;prior=copy.deepcopy(report['approach_proof'])
|
||||
report['look_proof']={'baseline_samples':[20,21],'response_sample':22}
|
||||
report['events']=[]
|
||||
o=SimpleNamespace(ready_at=2.,ready_real=32000,queued_real=30000,nonce='N',approach_data=lambda:('raw',{}),
|
||||
look_records=[{'kind':'sample','sample':21,'direction':scene.calibration['baseline_direction']},
|
||||
{'kind':'sample','sample':22,'direction':scene.calibration['response_direction']}],
|
||||
alignment_snapshots=scene.observe,alignment_previous=None)
|
||||
mouse=SimpleNamespace(alignment_move=lambda dx,dy,guard,deadline,clock:scene.move(dx,dy,guard,deadline=deadline))
|
||||
with patch('prison_approach_evidence.validate_approach_evidence',return_value=scene.accepted):
|
||||
proof.run_alignment(scene,SimpleNamespace(require_idle=scene.idle),o,mouse,report,scene.clock,scene.sleep)
|
||||
self.assertTrue(report['alignment_proof']['completed']);self.assertGreater(len(scene.motions),0)
|
||||
self.assertTrue(report['input_sent']);self.assertEqual(report['events'],[])
|
||||
self.assertEqual(report['approach_proof'],prior)
|
||||
self.assertLessEqual(report['alignment_proof']['deadline_monotonic'],39.)
|
||||
with patch('prison_approach_evidence.validate_approach_evidence',return_value=scene.accepted):
|
||||
with self.assertRaisesRegex(ValueError,'already exists'):
|
||||
proof.run_alignment(scene,SimpleNamespace(require_idle=scene.idle),o,mouse,report,scene.clock,scene.sleep)
|
||||
|
||||
def test_conditional_hashes_and_compaction_preserve_indexed_approach(self):
|
||||
import copy,hashlib
|
||||
report={'approach_requested':True,'alignment_requested':True,'approach_proof':{'final_sample':100,'samples':[{'sample':100,'real_ms':12000,'received_monotonic':10,'origin':[1,2,3]}]},'alignment_proof':{'completed':False,'samples':[{'sample':100,'real_ms':12000,'received_monotonic':10,'camera':[1,3,3]}]}}
|
||||
prior=copy.deepcopy(report);encoded=proof.report_for_write(report)
|
||||
self.assertEqual(report,prior);self.assertEqual(encoded['approach_proof']['final_sample'],100)
|
||||
self.assertEqual(encoded['approach_proof']['samples'],encoded['alignment_proof']['samples'])
|
||||
self.assertEqual(set(encoded['alignment_proof']['samples'][0]),{'sample','real_ms','received_monotonic'})
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root=Path(folder)
|
||||
for name in proof.ALIGNMENT_MODULES:(root/name).write_text(name)
|
||||
with patch.object(proof,'__file__',str(root/'headless_input_proof.py')):
|
||||
values=proof.module_hashes(proof.ALIGNMENT_MODULES)
|
||||
self.assertEqual(set(values),{'prison_alignment_proof.py','prison_alignment_evidence.py'})
|
||||
self.assertEqual(values['prison_alignment_proof.py'],hashlib.sha256(b'prison_alignment_proof.py').hexdigest())
|
||||
def test_alignment_watchdog_is_separate_and_cannot_renew(self):
|
||||
events=[{'kind':'report','metadata':{}},{'kind':'child','identity':{}},
|
||||
{'kind':'approach-pending','deadline_monotonic':5},{'kind':'approach-finished','successful':True},
|
||||
{'kind':'alignment-pending','deadline_monotonic':9},{'kind':'alignment-pending','deadline_monotonic':12}]
|
||||
class Pipe:
|
||||
def poll(self,*a):return bool(events)
|
||||
def recv(self):return events.pop(0)
|
||||
def close(self):pass
|
||||
class Process:
|
||||
pid=123;exitcode=0;alive=True;terminated=0
|
||||
def start(self):pass
|
||||
def is_alive(self):return self.alive
|
||||
def terminate(self):self.terminated+=1
|
||||
def join(self,*a):self.alive=False
|
||||
def kill(self):self.alive=False
|
||||
child=Process();pipe=Pipe();clock=Clock()
|
||||
def ticking():clock.now+=.5;return clock.now
|
||||
context=SimpleNamespace(Pipe=lambda **k:(pipe,pipe),Process=lambda **k:child)
|
||||
with patch.object(proof.multiprocessing,'get_context',return_value=context),patch.object(proof.time,'monotonic',side_effect=ticking),patch.object(proof.signal,'signal'),patch.object(proof,'stop_exact_child') as stop,patch.object(proof,'supervision_failure') as failed:
|
||||
self.assertEqual(proof.supervise([]),1);self.assertEqual(child.terminated,1)
|
||||
self.assertGreaterEqual(clock.now,9);self.assertLessEqual(clock.now,9.5)
|
||||
stop.assert_called_once();self.assertIsNone(failed.call_args.args[1])
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user