Files
framebridge/source/tests/test_adaptive_runtime.py

171 lines
9.7 KiB
Python

"""Adaptive text/graphical selection; inert CUA stubs never send user input."""
import base64
import hashlib
import json
import os
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from framebridge_expanded import adapter
from framebridge_expanded import config
from framebridge_expanded import display_context as dc
from framebridge_expanded import runtime
def physical(**changes):
data=dict(kind='physical-x11',physical=True,session_id='fixture-session',uid=os.getuid(),
leader=123,active_vt='tty7',seat='seat0',vtnr=7,display=':0',authority='/private/fixture.Xauthority')
data.update(changes)
return data
def capture_output(valid=True):
if not valid: return 'VISUAL_CAPTURE_GATE=PASS\nMIME=image/jpeg\nBASE64=invalid'
pixels=b'\xff\xd8synthetic-unit-fixture\xff\xd9'
return '\n'.join(['RESULTADO_FINAL=CUA_VISUAL_OBSERVE_PASS','VISUAL_CAPTURE_GATE=PASS','MIME=image/jpeg','BYTES='+str(len(pixels)),
'SHA256='+hashlib.sha256(pixels).hexdigest(),'BASE64='+base64.b64encode(pixels).decode()])
class AdaptiveDisplayTests(unittest.TestCase):
def test_auto_missing_graphical_tools_selects_text_and_clears_inherited_display(self):
env={'DISPLAY':':99','XAUTHORITY':'/unrelated'}
with patch.object(dc,'_physical',return_value=physical()), patch.object(dc,'missing_graphical_commands',return_value=['xdotool']):
dc.configure(env,'auto','/profile')
self.assertEqual(dc.describe(env)['kind'],'text')
self.assertNotIn('DISPLAY',env)
self.assertNotIn('XAUTHORITY',env)
self.assertFalse(dc.private_display_required(env,'auto',False))
def test_text_to_graphical_and_back_preserves_profile_and_requires_new_observation(self):
env={};dc.bind_text(env,'/profile')
with patch.object(dc,'_physical',return_value=physical()),patch.object(dc,'missing_graphical_commands',return_value=[]):
dc.prepare(env)
self.assertEqual(dc.check(env)['kind'],'physical-x11')
self.assertTrue(dc.describe(env)['needs_observation'])
dc.prepare(env)
self.assertTrue(dc.describe(env)['needs_observation'])
with patch.object(dc,'_physical',side_effect=dc.DisplayContextError('NO_SESSION')):
dc.prepare(env)
self.assertEqual(dc.describe(env)['kind'],'text')
self.assertEqual(dc.describe(env)['profile'],'/profile')
with self.assertRaisesRegex(dc.DisplayContextError,'CUA_DISPLAY_UNAVAILABLE'):
dc.check(env)
def stub(self,env,valid=True):
calls=[]
engine=SimpleNamespace()
def native(payload,timeout_s=60):
calls.append(payload)
return {'executed':True,'output':capture_output(valid)}
engine.native_cua_roundtrip=native
def handle(request,job):
response=engine.native_cua_roundtrip(request.get('cua',{}))
return {'ok':response['executed']}
engine.handle_cua_request=handle
adapter.install_adapter_guard(engine,env=env)
return engine,calls
def test_transition_blocks_input_until_successful_observation_then_allows_input(self):
env={};dc.bind_text(env,'/profile');engine,calls=self.stub(env)
with patch.object(dc,'_physical',return_value=physical()),patch.object(dc,'missing_graphical_commands',return_value=[]):
blocked=engine.handle_cua_request({'cua':{'action':'CLICK'}},'job1')
self.assertFalse(blocked['performed']);self.assertEqual(calls,[])
self.assertEqual(blocked['error'],'CUA_DISPLAY_CHANGED_OBSERVE_REQUIRED')
observed=engine.handle_cua_request({'cua':{'action':'OBSERVE'}},'job2')
self.assertTrue(observed['ok']);self.assertFalse(dc.describe(env).get('needs_observation',False))
self.assertTrue(engine.handle_cua_request({'cua':{'action':'CLICK'}},'job3')['ok'])
self.assertEqual(len(calls),2)
def test_invalid_observation_does_not_unlock_input_after_transition(self):
env={};dc.bind_text(env,'/profile');engine,calls=self.stub(env,valid=False)
with patch.object(dc,'_physical',return_value=physical()),patch.object(dc,'missing_graphical_commands',return_value=[]):
engine.handle_cua_request({'cua':{'action':'OBSERVE'}},'job1')
self.assertTrue(dc.describe(env)['needs_observation'])
blocked=engine.handle_cua_request({'cua':{'action':'CLICK'}},'job2')
self.assertFalse(blocked['performed']);self.assertEqual(len(calls),1)
def test_session_changes_during_observation_keep_input_locked(self):
env={};dc.bind_text(env,'/profile');engine,calls=self.stub(env)
with patch.object(dc,'_physical',side_effect=[physical(),physical(),physical(session_id='different')]),patch.object(dc,'missing_graphical_commands',return_value=[]):
result=engine.handle_cua_request({'cua':{'action':'OBSERVE'}},'job1')
self.assertFalse(result['ok'])
self.assertEqual(result['error'],'CUA_FOREGROUND_SESSION_CHANGED')
self.assertTrue(dc.describe(env)['needs_observation'])
def test_session_change_at_observation_release_returns_safe_block_and_keeps_input_locked(self):
env={};dc.bind_text(env,'/profile');engine,calls=self.stub(env)
with patch.object(dc,'_physical',side_effect=[physical(),physical(),physical(),physical(session_id='different')]),patch.object(dc,'missing_graphical_commands',return_value=[]):
result=engine.handle_cua_request({'cua':{'action':'OBSERVE'}},'job1')
self.assertFalse(result['ok'])
self.assertFalse(result['performed'])
self.assertFalse(result['input_may_have_been_sent'])
self.assertEqual(result['error'],'CUA_FOREGROUND_SESSION_CHANGED')
self.assertTrue(dc.describe(env)['needs_observation'])
with patch.object(dc,'_physical',return_value=physical(session_id='different')),patch.object(dc,'missing_graphical_commands',return_value=[]):
blocked=engine.handle_cua_request({'cua':{'action':'CLICK'}},'job2')
self.assertFalse(blocked['performed'])
self.assertEqual(blocked['error'],'CUA_DISPLAY_CHANGED_OBSERVE_REQUIRED')
self.assertEqual(len(calls),1)
class AdaptiveCoreTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory(prefix='fb-adaptive-test-')
self.state=Path(self.temp.name)
config.save_config(self.state,config.default_config())
self.env={'PATH':'/fixture/bin'}
dc.bind_text(self.env,self.state)
def tearDown(self): self.temp.cleanup()
def doctor(self,missing,*,compatibility=None):
def which(name,path=None): return None if name in missing else '/fixture/bin/'+name
# Graphical-capability tests use an eligible artifact fixture. Source
# checkouts do not contain the installed vendor layout or manifest.
eligibility = compatibility if compatibility is not None else {
'ok': True, 'profile_id': 'debian13-amd64-py313', 'issues': [], 'warnings': []}
with patch.object(runtime,'environment',return_value=dict(self.env)),patch.object(runtime.shutil,'which',side_effect=which),patch('framebridge_expanded.network.inspect',return_value={}),patch('framebridge_expanded.compatibility.assess',return_value=eligibility):
return runtime.doctor(self.state)
def test_optional_gui_dependencies_do_not_block_text_tools(self):
result=self.doctor(set(dc.GRAPHICAL_COMMANDS+dc.VIRTUAL_COMMANDS+('magick','convert','gnome-terminal','xterm')))
self.assertTrue(result['ok'])
self.assertEqual(result['missing_commands'],[])
self.assertTrue(result['capabilities']['fbexec'])
self.assertTrue(result['capabilities']['fbterm'])
self.assertTrue(result['capabilities']['cua_tty_module'])
self.assertFalse(result['capabilities']['graphical_cua'])
self.assertTrue(result['optional_missing_commands']['graphical_cua'])
def test_text_mode_does_not_bypass_incompatible_artifact(self):
rejected = {'ok': False, 'profile_id': None, 'issues': ['Fixture artifact ABI mismatch'], 'warnings': []}
result = self.doctor(set(dc.GRAPHICAL_COMMANDS + dc.VIRTUAL_COMMANDS), compatibility=rejected)
self.assertFalse(result['ok'])
self.assertIn('Fixture artifact ABI mismatch', result['issues'])
self.assertEqual([], result['missing_commands'])
self.assertFalse(result['capabilities']['graphical_cua'])
def test_missing_core_command_still_blocks_startup(self):
result=self.doctor({'bash'})
self.assertFalse(result['ok']);self.assertIn('bash',result['missing_commands'])
def test_explicit_private_desktop_requires_its_optional_components(self):
current=config.load_config(self.state);current['headless']='yes';config.save_config(self.state,current)
result=self.doctor({'Xvfb'})
self.assertFalse(result['ok']);self.assertIn('Xvfb',result['missing_commands'])
def test_text_manager_starts_core_session_without_xvfb_or_window_manager(self):
spawned=[]
def spawn(command,env,state,role):
spawned.append((command,role))
return SimpleNamespace(poll=lambda:None),{'role':role}
with patch.object(runtime,'environment',return_value=dict(self.env)),patch.object(runtime,'_display_ok',return_value=False),patch.object(runtime,'_spawn',side_effect=spawn),patch.object(runtime,'_atomic'),patch.object(runtime,'_terminate'),patch.object(runtime.signal,'signal'),patch.object(runtime,'_STOP',True):
runtime._manager(self.state)
self.assertEqual([role for _,role in spawned],['private-session'])
self.assertEqual(spawned[0][0][0],'dbus-run-session')
if __name__=='__main__': unittest.main()