341 lines
29 KiB
Python
341 lines
29 KiB
Python
"""Optional mouse-proof launch and evidence boundaries; never launch a display."""
|
|
import copy
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
from types import SimpleNamespace, ModuleType
|
|
import unittest
|
|
from unittest.mock import patch
|
|
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
|
|
import headless_game as r
|
|
import test_headless_input_exercise_runner as fixtures
|
|
from test_headless_input_exercise_runner import NONCE
|
|
|
|
class LookRunner(unittest.TestCase):
|
|
def test_gate_observation_is_independently_explicit_and_nonce_bound(self):
|
|
base=['gamescope','--backend','headless','--','/proton','run','/game']
|
|
self.assertNotIn('-errOrdinaryGateObserve',r.input_observer_command(base,'chat',None,NONCE))
|
|
self.assertEqual(r.input_observer_command(base,'chat',None,NONCE,gate=True)[-2:],['-errOrdinaryGateObserve',NONCE])
|
|
with self.assertRaises(ValueError):r.input_observer_command(base,'chat',None,NONCE,gate=1)
|
|
with self.assertRaises(ValueError):r.input_observer_command(base+['-errOrdinaryGateObserve','foreign'],'chat',None,NONCE)
|
|
with self.assertRaises(ValueError):r.validate_input_observer(SimpleNamespace(observe_prison_gate=True))
|
|
r.validate_input_observer(SimpleNamespace(observe_prison_gate=True,observe_player_input=True,project='chat',settle=50))
|
|
|
|
def test_optin_requires_all_existing_input_guards_before_side_effects(self):
|
|
valid=dict(exercise_relative_look=True,exercise_player_input=True,observe_player_input=True,inspect_headless_display=True,project='chat',settle=50)
|
|
r.validate_input_exercise(SimpleNamespace(**valid))
|
|
for field,value in [('exercise_player_input',False),('observe_player_input',False),('inspect_headless_display',False),('project',r.CONTROL),('settle',49),('packed_core',Path('/packed'))]:
|
|
args=SimpleNamespace(**{**valid,field:value})
|
|
with self.subTest(field=field),patch.object(r,'assert_no_game',side_effect=AssertionError('must refuse before process access')):
|
|
with self.assertRaises(ValueError):r.run(args)
|
|
r.validate_input_exercise(SimpleNamespace())
|
|
|
|
def test_native_flag_is_explicit_bound_and_default_has_no_look(self):
|
|
base=['gamescope','--backend','headless','--','/proton','run','/game']
|
|
ordinary=r.input_observer_command(base,'chat',None,NONCE)
|
|
self.assertNotIn('-errOrdinaryLookObserve',ordinary)
|
|
look=r.input_observer_command(base,'chat',None,NONCE,True)
|
|
self.assertEqual(look[-2:],['-errOrdinaryLookObserve',NONCE]);self.assertEqual(look[:-2],ordinary)
|
|
for command,flag in [(base,1),(look,True),(base+['-errOrdinaryLookObserve','foreign'],False)]:
|
|
with self.assertRaises(ValueError):r.input_observer_command(command,'chat',None,NONCE,flag)
|
|
|
|
def test_helper_optin_requires_exact_matching_native_nonce_and_keeps_private_boundary(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
|
for name in ('headless_input_proof.py','headless_display_probe.py','prison_approach_proof.py','ordinary_gate_evidence.py','ordinary_look_evidence.py','prison_approach_evidence.py'):(root/'tools'/name).write_text('# fake')
|
|
base=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE]
|
|
for native,flag,valid in [(base,False,True),(base,True,False),(base+['-errOrdinaryLookObserve',NONCE],True,True),(base+['-errOrdinaryLookObserve','foreign'],True,False),(base+['-errOrdinaryLookObserve',NONCE],False,False),(base+['-errOrdinaryLookObserve'],True,False),(base+['-errOrdinaryLookObserve',NONCE,'-errOrdinaryLookObserve',NONCE],True,False)]:
|
|
wrapped,display=r.display_probe_command(native,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
|
|
if not valid:
|
|
with self.assertRaises(ValueError):r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,flag)
|
|
continue
|
|
command,context=r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,flag)
|
|
self.assertEqual(command[:4],base[:4]);self.assertEqual(command[-len(native[4:]):],native[4:])
|
|
self.assertEqual('--enable-private-look-proof' in command,flag);self.assertEqual(context.get('look_requested',False),flag)
|
|
|
|
def test_final_evidence_is_independent_and_earlier_failures_survive(self):
|
|
for verified,prior in [(True,'game-started'),(False,'game-started'),(True,'game-failed'),(False,'cleanup-failed')]:
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
|
context['look_requested']=True;report['look_requested']=True;report['look_proof']={'requested':True,'completed':True}
|
|
raw='native sealed console bytes';(session/'console.log').write_text(raw);Path(context['report']).write_text(json.dumps(report))
|
|
module=ModuleType('ordinary_look_evidence');calls=[]
|
|
def validate(*args):calls.append(args);return {'verified':verified,'errors':[] if verified else ['native turn missing']}
|
|
module.validate_look_evidence=validate
|
|
with patch.dict(sys.modules,{'ordinary_look_evidence':module}):
|
|
result,proof=r.input_exercise_result(prior,context,session,observation,{'verified':True})
|
|
self.assertEqual(calls,[(raw,NONCE,report['look_proof'],report['events'])])
|
|
self.assertEqual(proof['verified'],verified)
|
|
self.assertEqual(result,'input-exercise-incomplete' if not verified and prior=='game-started' else prior)
|
|
self.assertEqual((session/'input-proof.json').read_bytes(),Path(context['report']).read_bytes())
|
|
|
|
def test_unrequested_or_mismatched_mouse_claim_cannot_qualify(self):
|
|
for request,reported,payload in [(False,True,{}),(True,False,None),(False,False,{}),(False,1,None)]:
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
|
context['look_requested']=request;report['look_requested']=reported
|
|
if payload is not None:report['look_proof']=payload
|
|
Path(context['report']).write_text(json.dumps(report))
|
|
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
|
self.assertFalse(proof['verified']);self.assertEqual(result,'input-exercise-incomplete')
|
|
|
|
class ApproachRunner(unittest.TestCase):
|
|
def test_wider_viewport_only_changes_owned_headless_command(self):
|
|
with self.assertRaises(ValueError):r.validate_input_exercise(SimpleNamespace(approach_wide_display=True))
|
|
args=(r.ROOT,Path('/steam'),Path('/compat'),Path('/profile'),'chat')
|
|
normal=r.command_for(*args);wide=r.command_for(*args,private_width=2560)
|
|
self.assertEqual(wide[:3],['gamescope','--backend','headless'])
|
|
self.assertEqual(wide[wide.index('-W')+1],'2560');self.assertEqual(wide[wide.index('-w')+1],'2560')
|
|
replaced=wide.copy();replaced[wide.index('-W')+1]='1280';replaced[wide.index('-w')+1]='1280';self.assertEqual(replaced,normal)
|
|
for bad in (True,1920,0,4096):
|
|
with self.assertRaises(ValueError):r.command_for(*args,private_width=bad)
|
|
|
|
def test_native_gate_look_and_route_optins_are_all_required(self):
|
|
valid=dict(exercise_prison_approach=True,exercise_relative_look=True,observe_prison_gate=True,exercise_player_input=True,observe_player_input=True,inspect_headless_display=True,project='chat',settle=50)
|
|
r.validate_input_exercise(SimpleNamespace(**valid))
|
|
for missing in ('exercise_relative_look','observe_prison_gate','exercise_player_input','observe_player_input','inspect_headless_display'):
|
|
with self.assertRaises(ValueError):r.validate_input_exercise(SimpleNamespace(**{**valid,missing:False}))
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
|
for name in ('headless_input_proof.py','headless_display_probe.py','prison_approach_proof.py','ordinary_gate_evidence.py','ordinary_look_evidence.py','prison_approach_evidence.py'):(root/'tools'/name).write_text('# fake')
|
|
native=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE,'-errOrdinaryLookObserve',NONCE,'-errOrdinaryGateObserve',NONCE]
|
|
wrapped,display=r.display_probe_command(native,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
|
|
command,context=r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,True,True)
|
|
self.assertIn('--enable-private-approach-proof',command);self.assertTrue(context['approach_requested'])
|
|
for bad in (native[:-2],native[:-1]+['foreign']):
|
|
wrapped,display=r.display_probe_command(bad,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
|
|
with self.assertRaises(ValueError):r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,True,True)
|
|
|
|
def test_route_uses_independent_raw_proof_and_preserves_prior_failure(self):
|
|
for verified,prior in [(True,'game-started'),(False,'game-started'),(True,'game-failed')]:
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
|
context.update(look_requested=True,approach_requested=True,approach_module_sha256=r.approach_module_hashes(r.ROOT));report['approach_module_sha256']=context['approach_module_sha256'];report.update(look_requested=True,approach_requested=True,look_proof={'completed':True})
|
|
report['events']=report['events'][:2];report['observer']['events']=['READY'];report['approach_proof']={'events':copy.deepcopy(report['events'])}
|
|
observation['inventory_open_observed']=False;observation['inventory_close_observed']=False
|
|
observation['movement']={'action_observed':False,'physical_motion_observed':False,'released_input_observed':False}
|
|
# The legacy summary measures movement only before INVENTORY_OPEN. The
|
|
# separate approach validator must own raw movement evidence in this mode.
|
|
(session/'console.log').write_text('raw native');Path(context['report']).write_text(json.dumps(report))
|
|
module=ModuleType('prison_approach_evidence');calls=[]
|
|
def validate(*args):calls.append(args);return {'verified':verified,'errors':[]}
|
|
module.validate_approach_evidence=validate
|
|
with patch.dict(sys.modules,{'prison_approach_evidence':module}):
|
|
result,proof=r.input_exercise_result(prior,context,session,observation,{'verified':True})
|
|
self.assertEqual(calls,[('raw native',NONCE,report['look_proof'],report['approach_proof'])]);self.assertEqual(proof['verified'],verified)
|
|
self.assertEqual(result,'input-exercise-incomplete' if not verified and prior=='game-started' else prior)
|
|
|
|
def test_unrequested_or_mismatched_approach_does_not_qualify(self):
|
|
for payload in ({'approach_requested':True},{'approach_proof':{}},{'approach_requested':1}):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder));report.update(payload)
|
|
Path(context['report']).write_text(json.dumps(report))
|
|
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
|
self.assertFalse(proof['verified']);self.assertEqual(result,'input-exercise-incomplete')
|
|
|
|
class AlignmentRunner(unittest.TestCase):
|
|
def test_alignment_requires_explicit_approach_and_separate_hashes(self):
|
|
with self.assertRaises(ValueError):r.validate_input_exercise(SimpleNamespace(exercise_prison_alignment=True))
|
|
valid=dict(exercise_prison_alignment=True,exercise_prison_approach=True,exercise_relative_look=True,observe_prison_gate=True,exercise_player_input=True,observe_player_input=True,inspect_headless_display=True,project='chat',settle=50)
|
|
r.validate_input_exercise(SimpleNamespace(**valid))
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
|
names=('headless_input_proof.py','headless_display_probe.py','prison_approach_proof.py','ordinary_gate_evidence.py','ordinary_look_evidence.py','prison_approach_evidence.py','prison_alignment_proof.py','prison_alignment_evidence.py')
|
|
for name in names:(root/'tools'/name).write_text('# fake')
|
|
native=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE,'-errOrdinaryLookObserve',NONCE,'-errOrdinaryGateObserve',NONCE]
|
|
wrapped,display=r.display_probe_command(native,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
|
|
args=(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display)
|
|
command,context=r.input_exercise_command(*args,True,True,True)
|
|
self.assertIn('--enable-private-alignment-proof',command);self.assertTrue(context['alignment_requested'])
|
|
self.assertEqual(set(context['alignment_module_sha256']),set(names[-2:]))
|
|
for look,approach,alignment in ((True,False,True),(True,True,1)):
|
|
with self.assertRaises(ValueError):r.input_exercise_command(*args,look,approach,alignment)
|
|
|
|
def test_final_alignment_is_independent_and_preserves_failures(self):
|
|
for verified,prior in ((True,'game-started'),(False,'game-started'),(True,'cleanup-failed')):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
|
context.update(look_requested=True,approach_requested=True,alignment_requested=True,approach_module_sha256=r.approach_module_hashes(r.ROOT),alignment_module_sha256=r.alignment_module_hashes(r.ROOT))
|
|
report.update({k:context[k] for k in ('look_requested','approach_requested','alignment_requested','approach_module_sha256','alignment_module_sha256')})
|
|
report.update(look_proof={'completed':True},alignment_proof={'completed':True})
|
|
report['events']=report['events'][:2];report['observer']['events']=['READY'];report['approach_proof']={'events':copy.deepcopy(report['events'])}
|
|
(session/'console.log').write_text('native');Path(context['report']).write_text(json.dumps(report))
|
|
approach=ModuleType('prison_approach_evidence');approach.validate_approach_evidence=lambda *a:{'verified':True}
|
|
alignment=ModuleType('prison_alignment_evidence');calls=[]
|
|
def validate(*args):calls.append(args);return {'verified':verified}
|
|
alignment.validate_alignment_evidence=validate
|
|
with patch.dict(sys.modules,{'prison_approach_evidence':approach,'prison_alignment_evidence':alignment}):
|
|
result,proof=r.input_exercise_result(prior,context,session,observation,{'verified':True})
|
|
self.assertEqual(calls,[('native',NONCE,report['look_proof'],report['approach_proof'],report['alignment_proof'])])
|
|
self.assertEqual(proof['verified'],verified)
|
|
self.assertEqual(result,'input-exercise-incomplete' if not verified and prior=='game-started' else prior)
|
|
|
|
def test_unrequested_alignment_and_changed_hashes_refuse(self):
|
|
for payload in ({'alignment_requested':True},{'alignment_requested':1},{'alignment_proof':{}}):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder));report.update(payload)
|
|
Path(context['report']).write_text(json.dumps(report))
|
|
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
|
self.assertFalse(proof['verified'])
|
|
|
|
def test_alignment_hash_mismatch_and_current_module_mutation_refuse(self):
|
|
for changed_current in (False,True):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
|
context.update(look_requested=True,approach_requested=True,alignment_requested=True,alignment_module_sha256=r.alignment_module_hashes(r.ROOT))
|
|
report.update(alignment_requested=True,alignment_module_sha256=dict(context['alignment_module_sha256']))
|
|
altered=dict(context['alignment_module_sha256']);altered['prison_alignment_proof.py']='0'*64
|
|
if not changed_current:report['alignment_module_sha256']=altered
|
|
Path(context['report']).write_text(json.dumps(report))
|
|
current=altered if changed_current else context['alignment_module_sha256']
|
|
with patch.object(r,'alignment_module_hashes',return_value=current):
|
|
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
|
self.assertFalse(proof['verified']);self.assertIn('unchanged module sources',proof['error'])
|
|
|
|
|
|
class GateChoicesRunner(unittest.TestCase):
|
|
def test_readonly_choices_require_gate_and_preserve_default_signature(self):
|
|
base=['gamescope','--backend','headless','--','/proton','run','/game']
|
|
previous=r.input_observer_command(base,'chat',None,NONCE,True,True)
|
|
self.assertNotIn('-errOrdinaryGateChoices',previous)
|
|
choices=r.input_observer_command(base,'chat',None,NONCE,True,True,True)
|
|
self.assertEqual(choices,previous+['-errOrdinaryGateChoices',NONCE])
|
|
valid=dict(observe_prison_choices=True,observe_prison_gate=True,observe_player_input=True,project='chat',settle=50)
|
|
r.validate_input_observer(SimpleNamespace(**valid))
|
|
r.validate_input_exercise(SimpleNamespace(**valid)) # no input is requested
|
|
for missing in ('observe_prison_gate','observe_player_input'):
|
|
with self.assertRaises(ValueError):r.validate_input_observer(SimpleNamespace(**{**valid,missing:False}))
|
|
with self.assertRaises(ValueError):r.validate_input_exercise(SimpleNamespace(**valid,exercise_player_input=True))
|
|
for gate,choice in [(False,True),(True,1),(True,'true')]:
|
|
with self.assertRaises(ValueError):r.input_observer_command(base,'chat',None,NONCE,gate=gate,choices=choice)
|
|
for extra in (['-errOrdinaryGateChoices',NONCE],['-errOrdinaryGateChoices','foreign'],['-errOrdinaryGateChoicesAlias',NONCE]):
|
|
with self.assertRaises(ValueError):r.input_observer_command(base+extra,'chat',None,NONCE,gate=True,choices=True)
|
|
|
|
def test_input_wrapper_accepts_only_two_exact_gate_flags_without_extra_input_mode(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
|
for name in ('headless_input_proof.py','headless_display_probe.py','prison_approach_proof.py','ordinary_gate_evidence.py','ordinary_look_evidence.py','prison_approach_evidence.py'):(root/'tools'/name).write_text('# fake')
|
|
native=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE,'-errOrdinaryLookObserve',NONCE,'-errOrdinaryGateObserve',NONCE]
|
|
def wrap(value,approach=True):
|
|
wrapped,display=r.display_probe_command(value,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
|
|
return r.input_exercise_command(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,True,approach)
|
|
baseline,context=wrap(native)
|
|
command,newcontext=wrap(native+['-errOrdinaryGateChoices',NONCE])
|
|
self.assertEqual(command,baseline+['-errOrdinaryGateChoices',NONCE])
|
|
self.assertEqual({k:v for k,v in newcontext.items() if k!='command'},{k:v for k,v in context.items() if k!='command'})
|
|
self.assertEqual(newcontext['command'],context['command']+['-errOrdinaryGateChoices',NONCE])
|
|
for extra in (['-errOrdinaryGateChoices'],['-errOrdinaryGateChoices','foreign'],['-errOrdinaryGateChoices',NONCE]*2,['-errOrdinaryGateObserve',NONCE],['-errOrdinaryGateChoicesAlias',NONCE]):
|
|
with self.subTest(extra=extra),self.assertRaises(ValueError):wrap(native+extra)
|
|
with self.assertRaises(ValueError):wrap(native[:-2]+['-errOrdinaryGateChoices',NONCE])
|
|
with self.assertRaises(ValueError):wrap(native+['-errOrdinaryGateChoices',NONCE],False)
|
|
|
|
class SelectionRunner(unittest.TestCase):
|
|
def make_selection(self,folder):
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,folder)
|
|
context.update(look_requested=True,approach_requested=True,alignment_requested=True,selection_requested=True,approach_module_sha256=r.approach_module_hashes(r.ROOT),alignment_module_sha256=r.alignment_module_hashes(r.ROOT),selection_module_sha256=r.selection_module_hashes(r.ROOT))
|
|
report.update({k:context[k] for k in ('look_requested','approach_requested','alignment_requested','selection_requested','approach_module_sha256','alignment_module_sha256','selection_module_sha256')})
|
|
report.update(look_proof={},facing_proof={},action_selection_proof={'wheel':{'down_monotonic':31.,'up_monotonic':31.2}},wheel_events=[{'button':5,'down':True,'sent':True,'monotonic':31.},{'button':5,'down':False,'sent':True,'monotonic':31.2}])
|
|
report['events']=report['events'][:2];report['observer']['events']=['READY'];report['approach_proof']={'events':copy.deepcopy(report['events'])}
|
|
(session/'console.log').write_text('native selection')
|
|
return session,context,report,observation
|
|
|
|
def test_selection_requires_alignment_and_native_choices(self):
|
|
with self.assertRaises(ValueError):r.validate_input_exercise(SimpleNamespace(exercise_prison_selection=True))
|
|
with self.assertRaises(ValueError):r.validate_input_exercise(SimpleNamespace(exercise_prison_selection=True,exercise_prison_alignment=True))
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
|
names=('headless_input_proof.py','headless_display_probe.py','prison_approach_proof.py','ordinary_gate_evidence.py','ordinary_look_evidence.py','prison_approach_evidence.py','prison_alignment_proof.py','prison_alignment_evidence.py','ordinary_gate_choices.py','prison_action_selection_proof.py','prison_action_selection_evidence.py')
|
|
for name in names:(root/'tools'/name).write_text('# fake')
|
|
native=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE,'-errOrdinaryLookObserve',NONCE,'-errOrdinaryGateObserve',NONCE]
|
|
for has_choices in (False,True):
|
|
args=native+(['-errOrdinaryGateChoices',NONCE] if has_choices else [])
|
|
wrapped,display=r.display_probe_command(args,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
|
|
call=(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display,True,True,True,True)
|
|
if not has_choices:
|
|
with self.assertRaises(ValueError):r.input_exercise_command(*call)
|
|
else:
|
|
cmd,context=r.input_exercise_command(*call);self.assertIn('--enable-private-selection-proof',cmd);self.assertEqual(set(context['selection_module_sha256']),set(names[-3:]))
|
|
|
|
def test_raw_selection_validator_owns_success_and_prior_failure_survives(self):
|
|
for accepted,prior in ((True,'game-started'),(False,'game-started'),(True,'cleanup-failed')):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=self.make_selection(Path(folder));Path(context['report']).write_text(json.dumps(report))
|
|
approach=ModuleType('prison_approach_evidence');approach.validate_approach_evidence=lambda *a:{'verified':True}
|
|
selected=ModuleType('prison_action_selection_evidence');calls=[]
|
|
def validate(*args, facing_profile='standard'):
|
|
self.assertEqual(facing_profile,'standard');calls.append(args);return {'verified':accepted}
|
|
selected.validate_selection_evidence=validate
|
|
with patch.dict(sys.modules,{'prison_approach_evidence':approach,'prison_action_selection_evidence':selected}):
|
|
outcome,proof=r.input_exercise_result(prior,context,session,observation,{'verified':True})
|
|
self.assertEqual(calls,[('native selection',NONCE,report['look_proof'],report['approach_proof'],report['facing_proof'],report['action_selection_proof'])]);self.assertEqual(proof['verified'],accepted)
|
|
self.assertEqual(outcome,'input-exercise-incomplete' if not accepted and prior=='game-started' else prior)
|
|
|
|
def test_changed_hash_or_unconfirmed_unpaired_extra_wheel_refuses(self):
|
|
for fault in ('hash','held','release','extra','wrong_button','unsent','time','finally'):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=self.make_selection(Path(folder))
|
|
if fault=='hash':report['selection_module_sha256']={}
|
|
elif fault=='held':report['unconfirmed_held_button']=5
|
|
elif fault=='release':report['wheel_release_errors']=['failed']
|
|
elif fault=='extra':report['wheel_events'].append(dict(report['wheel_events'][-1]))
|
|
elif fault=='wrong_button':report['wheel_events'][0]['button']=4
|
|
elif fault=='unsent':report['wheel_events'][0]['sent']=False
|
|
elif fault=='time':report['wheel_events'][0]['monotonic']=30.
|
|
else:report['wheel_events'][1]['finally']=True
|
|
Path(context['report']).write_text(json.dumps(report))
|
|
approach=ModuleType('prison_approach_evidence');approach.validate_approach_evidence=lambda *a:{'verified':True}
|
|
selected=ModuleType('prison_action_selection_evidence');selected.validate_selection_evidence=lambda *a:(_ for _ in ()).throw(AssertionError('malformed wheel reached validator'))
|
|
with patch.dict(sys.modules,{'prison_approach_evidence':approach,'prison_action_selection_evidence':selected}):
|
|
outcome,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
|
self.assertFalse(proof['verified'],fault)
|
|
|
|
def test_unrequested_wheel_and_facing_do_not_qualify(self):
|
|
for payload in ({'selection_requested':True},{'selection_requested':1},{'wheel_events':[{}]},{'action_selection_proof':{}},{'facing_proof':{}}):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder));report.update(payload);Path(context['report']).write_text(json.dumps(report))
|
|
outcome,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True});self.assertFalse(proof['verified'])
|
|
|
|
class CoarseApproachRunner(unittest.TestCase):
|
|
def test_coarse_explicit_command_flag_and_hash_bound_context(self):
|
|
with self.assertRaises(ValueError):r.validate_input_exercise(SimpleNamespace(exercise_prison_coarse_approach=True))
|
|
valid=dict(exercise_prison_coarse_approach=True,exercise_prison_approach=True,exercise_relative_look=True,observe_prison_gate=True,exercise_player_input=True,observe_player_input=True,inspect_headless_display=True,project='chat',settle=50)
|
|
r.validate_input_exercise(SimpleNamespace(**valid))
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
|
for name in ('headless_input_proof.py','headless_display_probe.py','prison_approach_proof.py','ordinary_gate_evidence.py','ordinary_look_evidence.py','prison_approach_evidence.py'):(root/'tools'/name).write_text('# fake')
|
|
native=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE,'-errOrdinaryLookObserve',NONCE,'-errOrdinaryGateObserve',NONCE]
|
|
wrapped,display=r.display_probe_command(native,root,Path('/compat'),profile,Path('/game'),NONCE,':0');args=(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display)
|
|
ordinary,normal=r.input_exercise_command(*args,look=True,approach=True)
|
|
coarse,context=r.input_exercise_command(*args,look=True,approach=True,coarse_approach=True)
|
|
self.assertNotIn('--enable-private-coarse-approach-proof',ordinary);self.assertFalse(normal.get('coarse_approach_requested',False))
|
|
self.assertTrue(context['coarse_approach_requested']);self.assertEqual(context['approach_module_sha256'],normal['approach_module_sha256'])
|
|
coarse.remove('--enable-private-coarse-approach-proof');self.assertEqual(coarse,ordinary)
|
|
for kwargs in ({'look':True,'coarse_approach':True},{'look':True,'approach':True,'coarse_approach':1}):
|
|
with self.assertRaises(ValueError):r.input_exercise_command(*args,**kwargs)
|
|
|
|
def test_final_result_refuses_unrequested_coarse_or_profile_substitution(self):
|
|
for requested,reported,profile in ((False,True,'coarse'),(False,False,'coarse'),(True,False,'coarse'),(True,True,'standard'),(False,1,'standard'),(True,True,'unknown')):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
|
context.update(look_requested=True,approach_requested=True,coarse_approach_requested=requested,approach_module_sha256=r.approach_module_hashes(r.ROOT))
|
|
report.update(look_requested=True,approach_requested=True,coarse_approach_requested=reported,approach_module_sha256=context['approach_module_sha256'],look_proof={})
|
|
report['events']=report['events'][:2];report['observer']['events']=['READY'];report['approach_proof']={'profile':profile,'events':copy.deepcopy(report['events'])}
|
|
Path(context['report']).write_text(json.dumps(report))
|
|
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
|
self.assertFalse(proof['verified']);self.assertEqual(result,'input-exercise-incomplete')
|
|
|
|
def test_matching_coarse_proof_still_requires_independent_raw_validation(self):
|
|
for okay in (False,True):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
|
|
context.update(look_requested=True,approach_requested=True,coarse_approach_requested=True,approach_module_sha256=r.approach_module_hashes(r.ROOT))
|
|
report.update(look_requested=True,approach_requested=True,coarse_approach_requested=True,approach_module_sha256=context['approach_module_sha256'],look_proof={})
|
|
report['events']=[{'key':'W','down':i%2==0,'monotonic':1+i*.1} for i in range(32)];report['observer']['events']=['READY'];report['approach_proof']={'profile':'coarse','events':copy.deepcopy(report['events'])}
|
|
(session/'console.log').write_text('raw native');Path(context['report']).write_text(json.dumps(report))
|
|
module=ModuleType('prison_approach_evidence');module.validate_approach_evidence=lambda *a:{'verified':okay}
|
|
with patch.dict(sys.modules,{'prison_approach_evidence':module}):result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
|
self.assertEqual(proof['verified'],okay)
|
|
|
|
if __name__=='__main__':unittest.main()
|