"""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') if __name__=='__main__':unittest.main()