FrameBridge 0.3.1 community source release

This commit is contained in:
FrameBridge Development committed 2026-10-26 22:09:18 -04:00
commit b23819fff1
1354 files changed
+343716

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
"""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):
def which(name,path=None): return None if name in missing else '/fixture/bin/'+name
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={}):
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_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()
+116
View File
@@ -0,0 +1,116 @@
"""Pure guide/catalog checks; no server, connection or node is started."""
import ast
import copy
import json
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import __version__
from framebridge_expanded import guide
SOURCE=Path(__file__).resolve().parents[1]
FRONT=SOURCE/'portable/front_door_mcp.py'
class FakeMCP:
def __init__(self,name,**kwargs):
self.name=name;self.options=kwargs
self._mcp_server=SimpleNamespace(version=None)
self._tool_manager=SimpleNamespace(_tools={})
self.annotations={}
def tool(self,**kwargs):
def register(fn):
self._tool_manager._tools[fn.__name__]=fn
self.annotations[fn.__name__]=kwargs.get('annotations')
return fn
return register
def guide_surface():
tree=ast.parse(FRONT.read_text())
names={'_registered_operations','framebridge_help','framebridge_status'}
keep=[]
for node in tree.body:
if isinstance(node,ast.Assign) and (
any(isinstance(t,ast.Name) and t.id=='mcp' for t in node.targets)
or any(isinstance(t,ast.Attribute) and t.attr=='version' for t in node.targets)):
keep.append(copy.deepcopy(node))
elif isinstance(node,ast.FunctionDef) and node.name in names:
keep.append(copy.deepcopy(node))
ns={'FastMCP':FakeMCP,'ToolAnnotations':lambda **kw:kw,'PRODUCT_VERSION':__version__,
'GUIDE_SCHEMA':guide.GUIDE_SCHEMA,'get_guide':guide.get_guide,
'socket':SimpleNamespace(gethostname=lambda:'fixture-receiver'),'HOST':'127.0.0.1','PORT':20865,
'ADAPTER_SOCKET':SimpleNamespace(is_socket=lambda:False)}
exec(compile(ast.fix_missing_locations(ast.Module(body=keep,type_ignores=[])),str(FRONT),'exec'),ns)
return ns
class GuideTests(unittest.TestCase):
def test_guide_versions_topics_and_bounds(self):
self.assertEqual(len(guide.topics()),10)
for topic in (*guide.topics(),'all'):
value=guide.get_guide(topic)
self.assertTrue(value['ok'])
self.assertEqual(value['version'],__version__)
self.assertTrue(value['read_only'])
self.assertFalse(value['capabilities_verified'])
self.assertLess(len(json.dumps(value).encode()),65536)
self.assertRegex(value['guide_sha256'],r'^[a-f0-9]{64}$')
def test_guide_reads_no_profile_or_network(self):
with mock.patch('builtins.open',side_effect=AssertionError('no file reads')), \
mock.patch('subprocess.run',side_effect=AssertionError('no processes')), \
mock.patch('socket.socket',side_effect=AssertionError('no sockets')):
self.assertTrue(guide.get_guide('start')['ok'])
def test_invalid_topic_is_bounded_actionable(self):
self.assertEqual(guide.get_guide('missing')['error'],'HELP_TOPIC_UNKNOWN')
self.assertEqual(guide.get_guide('x'*65)['error'],'HELP_TOPIC_INVALID')
with self.assertRaises(ValueError):guide.render_text('missing')
def test_guides_have_no_fixed_owner_paths_or_account(self):
text=json.dumps(guide.export_guide())
for marker in ['/home/gerabh','/home/gera/','gerabh1128@','GERAMEG','ASUS','GERANAS']:
self.assertNotIn(marker,text)
def test_human_agent_and_transcript_boundary_are_explicit(self):
text=guide.render_text('all')
self.assertIn('human operators and authorized agents',text)
self.assertIn('terminal transcript',text)
self.assertIn('not an authentic full-screen image',text)
self.assertIn('normal operating-system authentication',text)
self.assertIn('does not restrict GNU GPL freedoms',text)
def test_export_matches_installed_web_inputs(self):
data=json.loads((SOURCE/'docs/COMMAND-GUIDE.json').read_text())
self.assertEqual(data,guide.export_guide())
self.assertIn('FrameBridge '+__version__,(SOURCE/'docs/framebridge.1').read_text())
self.assertNotIn('Reconnecting requires entering the runtime key again',
(SOURCE/'docs/framebridge.1').read_text())
def test_development_guidance_preserves_license_freedom(self):
text=guide.render_text('security')
self.assertIn('separate source clone',text)
self.assertIn('do not present a modified build as an official release',text)
self.assertIn('GNU GPLv3-only',text)
class MCPGuideSurfaceTests(unittest.TestCase):
def test_init_invites_learning_and_has_product_version(self):
ns=guide_surface();mcp=ns['mcp']
self.assertIn("framebridge_help(topic='start')",mcp.options['instructions'])
self.assertEqual(mcp._mcp_server.version,__version__)
self.assertEqual(mcp.name,'FrameBridge')
def test_help_is_registered_readonly_and_bounded(self):
ns=guide_surface();value=ns['framebridge_help']('start')
self.assertTrue(value['ok']);self.assertEqual(value['receiver'],'fixture-receiver')
self.assertIn('framebridge_help',value['registered_operations'])
self.assertTrue(ns['mcp'].annotations['framebridge_help']['readOnlyHint'])
self.assertNotIn('adapter_socket',value)
def test_status_query_success_is_not_adapter_readiness(self):
ns=guide_surface();value=ns['framebridge_status']()
self.assertTrue(value['query_success'])
self.assertFalse(value['adapter_socket_present'])
self.assertEqual(value['adapter_ready'],'not_checked')
self.assertEqual(value['version'],__version__)
self.assertNotIn('adapter_socket',value)
def test_historical_lab_tools_are_not_publicly_decorated(self):
tree=ast.parse(FRONT.read_text())
byname={n.name:n for n in tree.body if isinstance(n,(ast.FunctionDef,ast.AsyncFunctionDef))}
for name in ['framebridge_exec_geranas','framebridge_cloud_dsm']:
self.assertFalse(byname[name].decorator_list)
self.assertTrue(byname['framebridge_exec'].decorator_list)
self.assertTrue(byname['framebridge_help'].decorator_list)
if __name__=='__main__':unittest.main()
+660
View File
@@ -0,0 +1,660 @@
import fcntl
import io
import json
import os
from pathlib import Path
import signal
import stat
import subprocess
import tempfile
import unittest
from unittest import mock
from framebridge_expanded import autostart, config, runtime, connection_background
ID = "tunnel_0123456789abcdef0123456789abcdef"
MARKER = "FRAMEBRIDGE-SYNTHETIC-NOT-A-REAL-KEY-AUTOSTART"
class AutostartTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.home = self.root / "home"
self.home.mkdir(mode=0o700)
self.state = self.home / "profile"
self.settings = config.default_config()
self.settings.update(tunnel_id=ID, headless="no")
config.save_config(self.state, self.settings)
self.addCleanup(mock.patch.stopall)
mock.patch.object(Path, "home", return_value=self.home).start()
self.calls = []
self.system_calls = []
self.command = mock.patch.object(autostart, "_command", side_effect=self.fake_command).start()
mock.patch.object(autostart, "_systemctl", side_effect=self.fake_systemctl).start()
mock.patch.object(autostart, "_linger", return_value=False).start()
mock.patch.object(autostart, "_enabled_units", return_value=[]).start()
mock.patch.object(autostart.os, "geteuid", return_value=1000).start()
mock.patch.object(autostart.resource, "setrlimit").start()
self.inputs = mock.patch("builtins.input", side_effect=["y"]).start()
self.key_prompt = mock.patch.object(autostart.getpass, "getpass", return_value=MARKER).start()
mock.patch.object(autostart, "status", return_value={"configured": True}).start()
mock.patch("sys.stdout", new_callable=io.StringIO).start()
def fake_command(self, args, **kwargs):
self.calls.append((args, kwargs))
if args[:2] == ["systemd-creds", "--version"]:
return subprocess.CompletedProcess(args, 0, b"systemd 257\n", b"")
if "encrypt" in args:
return subprocess.CompletedProcess(args, 0, b"CIPHERTEXT-ONLY-" * 20, b"")
return subprocess.CompletedProcess(args, 0, b"", b"")
def fake_systemctl(self, *args, **kwargs):
self.system_calls.append(args)
return subprocess.CompletedProcess(args, 0, b"", b"")
def test_enable_passes_key_only_by_anonymous_input_and_enables_boot_fallback(self):
before = (self.state / "config.json").read_bytes()
autostart.enable(self.state)
state, directory, unit = autostart._paths(self.state)
self.assertNotEqual(before, (self.state / "config.json").read_bytes())
self.assertEqual("auto", config.load_config(self.state)["headless"])
encryption = [(a, kw) for a, kw in self.calls if "encrypt" in a]
self.assertEqual(1, len(encryption))
args, options = encryption[0]
self.assertEqual(MARKER.encode(), options["data"])
self.assertNotIn(MARKER, " ".join(args))
self.assertIn("--with-key=host", args)
self.assertIn("--uid=" + str(os.getuid()), args)
for path in [directory / "credential.cred", directory / "installation.json", unit]:
self.assertEqual(0o600, stat.S_IMODE(path.stat().st_mode))
self.assertNotIn(MARKER.encode(), path.read_bytes())
self.assertNotIn("LoadCredentialEncrypted=", unit.read_text())
self.assertIn("StandardInput=null", unit.read_text())
self.assertIn(("enable", "--now", unit.name), self.system_calls)
autostart.enable(self.state)
self.assertEqual(1, self.key_prompt.call_count)
self.inputs.assert_called_once_with("Enable automatic startup? [y/N]: ")
def test_boot_fallback_restores_only_unchanged_profile_on_disable(self):
self.inputs.side_effect = ["y"]
autostart.enable(self.state)
self.assertEqual("auto", config.load_config(self.state)["headless"])
result = autostart.disable(self.state)
self.assertTrue(result["desktop_setting_restored"])
self.assertTrue(result["linger_restored"])
self.assertEqual("no", config.load_config(self.state)["headless"])
_, directory, unit = autostart._paths(self.state)
self.assertFalse(unit.exists())
self.assertFalse((directory / "credential.cred").exists())
def test_disable_keeps_desktop_changes_made_after_enable(self):
self.inputs.side_effect = ["y"]
autostart.enable(self.state)
settings = config.load_config(self.state)
settings["name"] = "changed-later"
config.save_config(self.state, settings)
result = autostart.disable(self.state)
self.assertFalse(result["desktop_setting_restored"])
self.assertEqual("changed-later", config.load_config(self.state)["name"])
self.assertEqual("auto", config.load_config(self.state)["headless"])
def test_modified_unit_is_not_stopped_or_removed(self):
autostart.enable(self.state)
_, _, unit = autostart._paths(self.state)
unit.write_text(unit.read_text() + "# external edit\n")
self.system_calls.clear()
with self.assertRaisesRegex(RuntimeError, "changed outside"):
autostart.disable(self.state)
self.assertEqual([], self.system_calls)
self.assertTrue(unit.exists())
def test_encryption_failure_never_creates_unit_or_disables_an_existing_service(self):
def failing(args, **kwargs):
if "encrypt" in args:
raise RuntimeError("Credential encryption failed.")
return self.fake_command(args, **kwargs)
self.command.side_effect = failing
with self.assertRaisesRegex(RuntimeError, "encryption failed"):
autostart.enable(self.state)
_, directory, unit = autostart._paths(self.state)
self.assertFalse(unit.exists())
self.assertFalse((directory / "credential.cred").exists())
self.assertFalse(any(x[0] == "disable" for x in self.system_calls))
def test_cancel_never_requests_credentials_or_privileges(self):
self.inputs.side_effect = ["cancel"]
result = autostart.enable(self.state)
self.assertEqual("cancelled", result["state"])
self.key_prompt.assert_not_called()
self.assertFalse(any(a[0] == "sudo" for a, _ in self.calls))
def test_default_no_keeps_profile_unchanged_without_privileges_or_key(self):
before = (self.state / "config.json").read_bytes()
self.inputs.side_effect = [""]
result = autostart.enable(self.state)
self.assertEqual({"configured": False, "state": "cancelled"}, result)
self.assertEqual(before, (self.state / "config.json").read_bytes())
self.key_prompt.assert_not_called()
self.assertFalse(any(args[0] == "sudo" for args, _ in self.calls))
self.assertEqual([], self.system_calls)
_, directory, unit = autostart._paths(self.state)
self.assertFalse(unit.exists())
self.assertFalse((directory / "credential.cred").exists())
def test_natural_confirmation_is_case_insensitive_and_requested_once(self):
self.inputs.side_effect = [" YeS "]
autostart.enable(self.state)
autostart.enable(self.state)
self.inputs.assert_called_once_with("Enable automatic startup? [y/N]: ")
self.key_prompt.assert_called_once_with("Connection key (hidden): ")
def test_setup_shows_bounded_progress_without_claiming_connected(self):
output = io.StringIO()
with mock.patch("sys.stdout", output):
autostart.enable(self.state)
text = output.getvalue()
self.assertLess(text.index("[1/3]"), text.index("[2/3]"))
self.assertLess(text.index("[2/3]"), text.index("[3/3]"))
self.assertLessEqual(len(text.splitlines()), 6)
self.assertTrue(all(len(line) <= 80 for line in text.splitlines()))
self.assertNotIn(MARKER, text)
self.assertNotIn("connected", text.lower())
self.assertNotIn("systemd", text)
self.assertNotIn("physical-only", text)
self.assertNotIn("Root access", text)
def test_eof_or_interrupt_at_confirmation_cancels_without_authentication(self):
for cancellation in (EOFError, KeyboardInterrupt):
with self.subTest(cancellation=cancellation.__name__):
self.inputs.side_effect = cancellation
result = autostart.enable(self.state)
self.assertEqual("cancelled", result["state"])
self.assertFalse(result["configured"])
self.key_prompt.assert_not_called()
self.assertFalse(any(args[0] == "sudo" for args, _ in self.calls))
self.assertEqual([], self.system_calls)
def _durable_settings(self, identifier=ID):
return {"account_label": "owner@example.test", "tunnel_id": identifier,
"control_plane_organization_id": "", "control_plane_url_path": ""}
def _durable_connect(self, key=MARKER, settings=None):
with mock.patch.object(connection_background, "status", return_value={"alive": False}), mock.patch.object(connection_background, "disconnect", return_value={"ok": True, "alive": False}):
return autostart.configure_connection(self.state, settings or self._durable_settings(), key)
def test_durable_connection_uses_supplied_key_once_without_extra_questions(self):
self._durable_connect()
self.inputs.assert_not_called()
self.key_prompt.assert_not_called()
encryptions = [(args, kw) for args, kw in self.calls if "encrypt" in args]
self.assertEqual(1, len(encryptions))
self.assertEqual(MARKER.encode(), encryptions[0][1]["data"])
self.assertNotIn(MARKER, " ".join(encryptions[0][0]))
_, directory, unit = autostart._paths(self.state)
for path in (self.state / "config.json", directory / "credential.cred", directory / "installation.json", unit):
self.assertNotIn(MARKER.encode(), path.read_bytes())
self.assertEqual("auto", config.load_config(self.state)["headless"])
self.assertIn(("enable", "--now", unit.name), self.system_calls)
def test_interrupt_reading_status_after_commit_keeps_enabled_service_and_reports_it(self):
with mock.patch.object(autostart, "status", side_effect=KeyboardInterrupt):
result = self._durable_connect()
self.assertEqual({"configured": True, "enabled": True, "interrupted_after_commit": True}, result)
_, directory, unit = autostart._paths(self.state)
self.assertTrue(unit.exists())
self.assertTrue((directory / "credential.cred").exists())
self.assertTrue((directory / "installation.json").exists())
self.assertIn(("enable", "--now", unit.name), self.system_calls)
self.assertNotIn(("disable", "--now", unit.name), self.system_calls)
def test_durable_connection_can_create_default_profile_in_the_transaction(self):
(self.state / "config.json").unlink()
self._durable_connect()
selected = config.load_config(self.state)
self.assertEqual("standalone", selected["role"])
self.assertEqual("auto", selected["headless"])
self.assertEqual("owner@example.test", selected["account_label"])
self.assertEqual(ID, selected["tunnel_id"])
def test_durable_encryption_failure_leaves_current_profile_and_service_alone(self):
original = (self.state / "config.json").read_bytes()
def failed(args, **kwargs):
if "encrypt" in args:
raise RuntimeError("Protection failed.")
return self.fake_command(args, **kwargs)
self.command.side_effect = failed
with mock.patch.object(connection_background, "disconnect") as disconnect:
with self.assertRaisesRegex(RuntimeError, "Protection failed"):
self._durable_connect()
self.assertEqual(original, (self.state / "config.json").read_bytes())
self.assertFalse(any(args[0] in {"stop", "enable", "disable"} for args in self.system_calls))
_, directory, unit = autostart._paths(self.state)
self.assertFalse(unit.exists())
self.assertFalse((directory / "credential.cred").exists())
def test_durable_replacement_encrypts_before_stop_and_replaces_public_identity(self):
self._durable_connect()
events = []
original_command = self.fake_command
def command(args, **kwargs):
if "encrypt" in args:
events.append("encrypt")
return subprocess.CompletedProcess(args, 0, b"REPLACEMENT-CIPHERTEXT-" * 20, b"")
return original_command(args, **kwargs)
def service(*args, **kwargs):
events.append(args[0])
if args[0] == "show":
return subprocess.CompletedProcess(args, 0, b"ActiveState=active\nUnitFileState=enabled\n", b"")
return self.fake_systemctl(*args, **kwargs)
live = {"alive": True}
def disconnect(state):
events.append("disconnect")
live["alive"] = False
return {"ok": True, "alive": False}
self.command.side_effect = command
new_id = "tunnel_replacement0123456789"
with mock.patch.object(autostart, "_systemctl", side_effect=service), mock.patch.object(connection_background, "status", side_effect=lambda state: dict(live)), mock.patch.object(connection_background, "disconnect", side_effect=disconnect):
autostart.configure_connection(self.state, self._durable_settings(new_id), MARKER + "-NEW")
self.assertLess(events.index("encrypt"), events.index("stop"))
self.assertLess(events.index("disconnect"), events.index("enable"))
self.assertEqual(new_id, config.load_config(self.state)["tunnel_id"])
_, directory, _ = autostart._paths(self.state)
self.assertEqual(b"REPLACEMENT-CIPHERTEXT-" * 20, (directory / "credential.cred").read_bytes())
def test_durable_replacement_failure_restores_exact_files_and_restarts_old_unit(self):
self._durable_connect()
_, directory, unit = autostart._paths(self.state)
paths = (self.state / "config.json", unit, directory / "credential.cred", directory / "installation.json", directory / "config-before.json")
originals = {p: p.read_bytes() for p in paths}
calls = []
def failed(*args, **kwargs):
calls.append(args)
if args[0] == "show":
return subprocess.CompletedProcess(args, 0, b"ActiveState=active\nUnitFileState=enabled\n", b"")
if args[:2] == ("enable", "--now"):
raise RuntimeError("New service did not start.")
return self.fake_systemctl(*args, **kwargs)
with mock.patch.object(autostart, "_systemctl", side_effect=failed):
with self.assertRaisesRegex(RuntimeError, "New service did not start"):
self._durable_connect(MARKER + "-NEW", self._durable_settings("tunnel_changed0123456789"))
self.assertEqual(originals, {p: p.read_bytes() for p in paths})
self.assertIn(("start", unit.name), calls)
self.assertIn(("enable", unit.name), calls)
def test_durable_connection_rejects_changed_existing_service_before_authentication(self):
self._durable_connect()
_, _, unit = autostart._paths(self.state)
unit.write_bytes(unit.read_bytes() + b"# external change\n")
self.calls.clear()
self.system_calls.clear()
with self.assertRaisesRegex(RuntimeError, "changed outside"):
self._durable_connect(MARKER + "-NEW")
self.assertEqual([], self.calls)
self.assertEqual([], self.system_calls)
def test_durable_invalid_key_or_public_fields_have_no_authentication_side_effect(self):
for key, settings in (("bad key", self._durable_settings()), (MARKER + "é", self._durable_settings()), (MARKER, {**self._durable_settings(), "private_key": "not-accepted"})):
with self.subTest(key_length=len(key)):
with self.assertRaises(ValueError):
self._durable_connect(key, settings)
self.assertEqual([], self.calls)
self.assertEqual([], self.system_calls)
def test_durable_failure_reports_previous_manual_connection_needs_reconnecting(self):
live = {"alive": True}
def disconnect(state):
live["alive"] = False
return {"ok": True, "alive": False}
def failed(*args, **kwargs):
if args[:2] == ("enable", "--now"):
raise RuntimeError("Start failed.")
return self.fake_systemctl(*args, **kwargs)
before = (self.state / "config.json").read_bytes()
with mock.patch.object(connection_background, "status", side_effect=lambda state: dict(live)), mock.patch.object(connection_background, "disconnect", side_effect=disconnect), mock.patch.object(autostart, "_systemctl", side_effect=failed):
with self.assertRaisesRegex(RuntimeError, "previous manually started connection was stopped"):
autostart.configure_connection(self.state, self._durable_settings(), MARKER)
self.assertEqual(before, (self.state / "config.json").read_bytes())
def test_durable_rotation_then_disable_preserves_new_connection_settings(self):
self._durable_connect()
replacement = self._durable_settings("tunnel_next0123456789")
replacement["account_label"] = "new@example.test"
self._durable_connect(MARKER + "-NEW", replacement)
autostart.disable(self.state)
selected = config.load_config(self.state)
self.assertEqual("no", selected["headless"])
self.assertEqual(replacement["tunnel_id"], selected["tunnel_id"])
self.assertEqual(replacement["account_label"], selected["account_label"])
def test_durable_rollback_attempts_old_restart_even_after_disable_failure(self):
self._durable_connect()
_, _, unit = autostart._paths(self.state)
calls = []
def failed(*args, **kwargs):
calls.append(args)
if args[0] == "show":
return subprocess.CompletedProcess(args, 0, b"ActiveState=active\nUnitFileState=enabled\n", b"")
if args[:2] == ("enable", "--now"):
raise RuntimeError("Start failed.")
return subprocess.CompletedProcess(args, 1 if args[:2] == ("disable", "--now") else 0, b"", b"")
with mock.patch.object(autostart, "_systemctl", side_effect=failed):
with self.assertRaisesRegex(RuntimeError, "restoration could not be fully verified"):
self._durable_connect(MARKER + "-NEW")
self.assertIn(("start", unit.name), calls)
self.assertIn(("enable", unit.name), calls)
def test_resume_saved_stopped_connection_asks_for_no_key_or_privilege_when_linger_exists(self):
self._durable_connect()
_, directory, unit = autostart._paths(self.state)
old_cipher = (directory / "credential.cred").read_bytes()
old_unit = unit.read_bytes()
self.calls.clear()
self.system_calls.clear()
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(connection_background, "disconnect", return_value={"ok": True, "alive": False}), mock.patch.object(connection_background, "status", return_value={"alive": False}):
autostart.resume_connection(self.state)
self.inputs.assert_not_called()
self.key_prompt.assert_not_called()
self.assertEqual([], self.calls)
self.assertEqual(old_cipher, (directory / "credential.cred").read_bytes())
self.assertEqual(old_unit, unit.read_bytes())
self.assertIn(("enable", "--now", unit.name), self.system_calls)
def test_resume_active_verified_service_returns_without_restarting(self):
self._durable_connect()
self.calls.clear()
self.system_calls.clear()
ready = {"configured": True, "enabled": True, "service_active": True, "runtime": {"running": True, "ready": True}, "connection": {"alive": True, "verified": True}}
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "status", return_value=ready):
self.assertEqual(ready, autostart.resume_connection(self.state))
self.assertEqual([], self.calls)
self.assertEqual([], self.system_calls)
def test_resume_rejects_public_tunnel_change_before_service_actions(self):
self._durable_connect()
changed = config.load_config(self.state)
changed["tunnel_id"] = "tunnel_changed0123456789"
config.save_config(self.state, changed)
self.calls.clear()
self.system_calls.clear()
with self.assertRaisesRegex(RuntimeError, "--replace"):
autostart.resume_connection(self.state)
self.assertEqual([], self.calls)
self.assertEqual([], self.system_calls)
def test_resume_rejects_changed_cipher_before_service_actions(self):
self._durable_connect()
_, directory, _ = autostart._paths(self.state)
cipher = directory / "credential.cred"
cipher.write_bytes(cipher.read_bytes() + b"changed")
self.calls.clear()
self.system_calls.clear()
with self.assertRaisesRegex(RuntimeError, "--replace"):
autostart.resume_connection(self.state)
self.assertEqual([], self.calls)
self.assertEqual([], self.system_calls)
def test_resume_legacy_without_any_profile_binding_requires_replace(self):
self._durable_connect()
_, directory, _ = autostart._paths(self.state)
record = autostart._manifest(directory)
record.pop("connection_settings_sha256")
record.pop("changed_config_sha256")
autostart._json(directory / "installation.json", record, replace=True)
self.calls.clear()
self.system_calls.clear()
with self.assertRaisesRegex(RuntimeError, "--replace"):
autostart.resume_connection(self.state)
self.assertEqual([], self.calls)
self.assertEqual([], self.system_calls)
def test_resume_legacy_exact_profile_binding_can_be_verified_without_new_key(self):
self._durable_connect()
_, directory, _ = autostart._paths(self.state)
record = autostart._manifest(directory)
record.pop("connection_settings_sha256")
autostart._json(directory / "installation.json", record, replace=True)
ready = {"configured": True, "enabled": True, "service_active": True, "runtime": {"running": True, "ready": True}, "connection": {"alive": True, "verified": True}}
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "status", return_value=ready):
self.assertEqual(ready, autostart.resume_connection(self.state))
self.key_prompt.assert_not_called()
def test_node_autostart_needs_no_chatgpt_key(self):
settings = config.default_config(role="node")
settings.update(headless="yes", broker_url="https://example.test:20878")
config.save_config(self.state, settings)
self.inputs.side_effect = ["yes"]
autostart.enable(self.state)
_, directory, unit = autostart._paths(self.state)
self.key_prompt.assert_not_called()
self.assertNotIn("LoadCredentialEncrypted", unit.read_text())
self.assertFalse((directory / "credential.cred").exists())
def test_private_file_reader_rejects_symlink_hardlink_fifo_and_public_file(self):
original = self.root / "original"
original.write_bytes(b"safe")
original.chmod(0o600)
linked = self.root / "linked"
linked.symlink_to(original)
with self.assertRaises(OSError):
autostart._read(linked)
linked.unlink()
os.link(original, linked)
with self.assertRaises(RuntimeError):
autostart._read(original)
linked.unlink()
os.mkfifo(linked, 0o600)
with self.assertRaises(RuntimeError):
autostart._read(linked)
original.chmod(0o644)
with self.assertRaises(RuntimeError):
autostart._read(original)
def test_configuration_lock_rejects_concurrent_enable(self):
_, directory, _ = autostart._paths(self.state)
fd = os.open(directory / "configuration.lock", os.O_RDWR | os.O_CREAT, 0o600)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
with self.assertRaisesRegex(RuntimeError, "already in progress"):
autostart.enable(self.state)
self.key_prompt.assert_not_called()
finally:
os.close(fd)
def test_unit_quotes_specifiers_and_dollars_without_shell_or_missing_package_loop(self):
text = autostart.unit_text(Path('/tmp/a % b ${HOME} "quoted"'), Path('/tmp/key % file'))
self.assertIn("%%", text)
self.assertIn("$${HOME}", text)
self.assertIn("ConditionPathExists=", text)
self.assertIn("LimitCORE=0", text)
self.assertIn("RestartSec=15", text)
self.assertNotIn("/bin/sh", text)
with self.assertRaises(ValueError):
autostart.unit_text(Path("/tmp/a\nExecStart=bad"), Path("/tmp/key"))
def test_encrypted_credential_uses_os_broker_pipe_without_plaintext_files(self):
import hashlib
_, directory, _ = autostart._paths(self.state)
cipher = b"SYNTHETIC-CIPHERTEXT-ONLY" * 8
autostart._write(directory / "credential.cred", cipher)
autostart._json(directory / "installation.json", {"schema": 1, "node": False, "credential_sha256": hashlib.sha256(cipher).hexdigest()})
def decrypt(args, **kwargs):
self.assertEqual(["systemd-creds", "decrypt", "--user", "--name=chatgpt-runtime-key", "-", "-"], args)
self.assertEqual(cipher, kwargs["data"])
self.assertEqual(15, kwargs["timeout"])
self.assertNotIn(MARKER, " ".join(args))
return subprocess.CompletedProcess(args, 0, MARKER.encode(), b"")
self.command.side_effect = decrypt
self.assertEqual(MARKER, autostart._credential(self.state))
for path in directory.iterdir():
if path.is_file():
self.assertNotIn(MARKER.encode(), path.read_bytes())
def test_changed_cipher_is_rejected_before_decryption(self):
import hashlib
_, directory, _ = autostart._paths(self.state)
cipher = b"SYNTHETIC-CIPHERTEXT-ONLY" * 8
autostart._write(directory / "credential.cred", cipher)
autostart._json(directory / "installation.json", {"schema": 1, "node": False, "credential_sha256": hashlib.sha256(cipher).hexdigest()})
(directory / "credential.cred").write_bytes(cipher + b"changed")
self.command.reset_mock()
with self.assertRaisesRegex(RuntimeError, "changed outside"):
autostart._credential(self.state)
self.command.assert_not_called()
def test_decrypt_failure_reports_no_plaintext_or_broker_output(self):
import hashlib
_, directory, _ = autostart._paths(self.state)
cipher = b"SYNTHETIC-CIPHERTEXT-ONLY" * 8
autostart._write(directory / "credential.cred", cipher)
autostart._json(directory / "installation.json", {"schema": 1, "node": False, "credential_sha256": hashlib.sha256(cipher).hexdigest()})
self.command.side_effect = RuntimeError("Autostart command failed: systemd-creds. No command output was exported.")
with self.assertRaisesRegex(RuntimeError, "No command output was exported"):
autostart._credential(self.state)
def test_disconnect_pauses_worker_without_stopping_local_services_until_unit_stop(self):
settings = config.load_config(self.state)
settings["headless"] = "auto"
config.save_config(self.state, settings)
states = []
checks = 0
def wait(delay):
nonlocal checks
checks += 1
runtime_stop.assert_not_called()
self.assertIn("disconnected", states)
autostart._STOP = True
with mock.patch.object(runtime, "start", return_value={"running": True, "ready": True}), mock.patch.object(runtime, "status", return_value={"running": True}), mock.patch.object(runtime, "stop") as runtime_stop, mock.patch.object(connection_background, "start", return_value=1) as start, mock.patch.object(connection_background, "status", return_value={"alive": False, "verified": False, "state": "stopped"}), mock.patch.object(connection_background, "disconnect") as disconnect, mock.patch.object(autostart, "_credential", return_value=MARKER), mock.patch.object(autostart, "_worker_status", side_effect=lambda directory, name: states.append(name)), mock.patch.object(autostart.time, "sleep", side_effect=wait):
self.assertEqual(0, autostart.worker(self.state))
self.assertEqual(1, checks)
self.assertEqual(1, start.call_args.kwargs["wait_timeout"])
runtime_stop.assert_called_once()
disconnect.assert_called_once()
def test_failed_tunnel_requests_service_restart_and_cleans_up(self):
settings = config.load_config(self.state)
settings["headless"] = "auto"
config.save_config(self.state, settings)
with mock.patch.object(runtime, "start", return_value={"running": True, "ready": True}), mock.patch.object(runtime, "stop") as runtime_stop, mock.patch.object(connection_background, "start", return_value=1), mock.patch.object(connection_background, "status", return_value={"alive": False, "state": "error"}), mock.patch.object(connection_background, "disconnect") as disconnect, mock.patch.object(autostart, "_credential", return_value=MARKER):
self.assertEqual(1, autostart.worker(self.state))
runtime_stop.assert_called_once()
disconnect.assert_called_once()
def test_physical_desktop_wait_requires_no_new_key_prompt(self):
from framebridge_expanded.display_context import DisplayContextError
states = []
def wait(delay):
autostart._STOP = True
with mock.patch.object(runtime, "start", side_effect=DisplayContextError("PHYSICAL_X11_UNAVAILABLE_NO_VIRTUAL_FALLBACK")), mock.patch.object(runtime, "stop"), mock.patch.object(autostart, "_credential") as credential, mock.patch.object(autostart, "_worker_status", side_effect=lambda directory, name: states.append(name)), mock.patch.object(autostart.time, "sleep", side_effect=wait):
self.assertEqual(0, autostart.worker(self.state))
self.assertIn("waiting_for_graphical_login", states)
credential.assert_not_called()
self.key_prompt.assert_not_called()
def test_enable_interrupt_rolls_back_unit_blob_and_exact_profile(self):
before = (self.state / "config.json").read_bytes()
def interrupted(*args, **kwargs):
if args[:2] == ("enable", "--now"):
raise KeyboardInterrupt()
return self.fake_systemctl(*args, **kwargs)
with mock.patch.object(autostart, "_systemctl", side_effect=interrupted):
result = autostart.enable(self.state)
self.assertEqual({"configured": False, "state": "cancelled"}, result)
_, directory, unit = autostart._paths(self.state)
self.assertEqual(before, (self.state / "config.json").read_bytes())
self.assertFalse(unit.exists())
self.assertFalse((directory / "credential.cred").exists())
self.assertFalse((directory / "installation.json").exists())
def test_disable_privilege_cancellation_preserves_complete_installation(self):
autostart.enable(self.state)
_, directory, unit = autostart._paths(self.state)
unit_before = unit.read_bytes()
cipher_before = (directory / "credential.cred").read_bytes()
def cancelled(args, **kwargs):
if args == ["sudo", "-v"]:
raise KeyboardInterrupt()
return self.fake_command(args, **kwargs)
self.command.side_effect = cancelled
with self.assertRaises(KeyboardInterrupt):
autostart.disable(self.state)
self.assertEqual(unit_before, unit.read_bytes())
self.assertEqual(cipher_before, (directory / "credential.cred").read_bytes())
self.assertTrue((directory / "installation.json").exists())
def test_unconfigured_manual_stop_does_not_create_autostart_directory(self):
other = self.home / "other-profile"
config.save_config(other, config.default_config())
self.assertFalse(autostart.mark_manual_stop(other))
self.assertFalse((other / "autostart").exists())
def test_runtime_crash_requests_service_restart(self):
with mock.patch.object(runtime, "start", return_value={"running": True, "ready": True}), mock.patch.object(runtime, "status", return_value={"running": False}), mock.patch.object(runtime, "stop") as runtime_stop, mock.patch.object(connection_background, "start", return_value=0), mock.patch.object(connection_background, "status", return_value={"alive": True, "verified": True, "state": "connected"}), mock.patch.object(connection_background, "disconnect") as disconnect, mock.patch.object(autostart, "_credential", return_value=MARKER):
self.assertEqual(1, autostart.worker(self.state))
runtime_stop.assert_called_once()
disconnect.assert_called_once()
def test_explicit_stop_intent_prevents_automatic_restart(self):
_, directory, _ = autostart._paths(self.state)
autostart._json(directory / "installation.json", {"schema": 1, "node": False})
def explicitly_stopped(state):
self.assertTrue(autostart.mark_manual_stop(state))
return {"running": False}
with mock.patch.object(runtime, "start", return_value={"running": True, "ready": True}), mock.patch.object(runtime, "status", side_effect=explicitly_stopped), mock.patch.object(runtime, "stop") as runtime_stop, mock.patch.object(connection_background, "start", return_value=0), mock.patch.object(connection_background, "status", return_value={"alive": True, "verified": True, "state": "connected"}), mock.patch.object(connection_background, "disconnect"), mock.patch.object(autostart, "_credential", return_value=MARKER):
self.assertEqual(0, autostart.worker(self.state))
runtime_stop.assert_called_once()
self.assertTrue(autostart.manual_stop_requested(self.state))
def test_stale_manual_stop_is_cleared_when_a_new_worker_starts(self):
_, directory, _ = autostart._paths(self.state)
autostart._json(directory / "installation.json", {"schema": 1, "node": False})
self.assertTrue(autostart.mark_manual_stop(self.state))
self.assertTrue(autostart.manual_stop_requested(self.state))
with mock.patch.object(runtime, "start", return_value={"running": True, "ready": True}) as start, mock.patch.object(runtime, "status", return_value={"running": False}), mock.patch.object(runtime, "stop"), mock.patch.object(connection_background, "start", return_value=0), mock.patch.object(connection_background, "status", return_value={"alive": True, "verified": True, "state": "connected"}), mock.patch.object(connection_background, "disconnect"), mock.patch.object(autostart, "_credential", return_value=MARKER):
self.assertEqual(1, autostart.worker(self.state))
start.assert_called_once()
self.assertFalse(autostart.manual_stop_requested(self.state))
def test_stop_for_current_boot_cancels_restart_job_but_preserves_unit_and_credential(self):
autostart.enable(self.state)
_, directory, unit = autostart._paths(self.state)
cipher = (directory / "credential.cred").read_bytes()
original_unit = unit.read_bytes()
self.system_calls.clear()
result = autostart.stop_for_current_boot(self.state)
self.assertEqual("stopped_for_current_boot", result["state"])
self.assertEqual([("stop", unit.name)], self.system_calls)
self.assertEqual(original_unit, unit.read_bytes())
self.assertEqual(cipher, (directory / "credential.cred").read_bytes())
self.assertTrue((directory / "installation.json").exists())
def test_stop_for_current_boot_refuses_changed_unit(self):
autostart.enable(self.state)
_, _, unit = autostart._paths(self.state)
unit.write_text(unit.read_text() + "# foreign change\n")
self.system_calls.clear()
with self.assertRaisesRegex(RuntimeError, "changed outside"):
autostart.stop_for_current_boot(self.state)
self.assertEqual([], self.system_calls)
self.assertTrue(unit.exists())
def test_stop_for_current_boot_unconfigured_does_not_create_directories(self):
other = self.home / "unmanaged"
config.save_config(other, config.default_config())
self.assertFalse(autostart.stop_for_current_boot(other)["configured"])
self.assertFalse((other / "autostart").exists())
self.assertEqual([], self.system_calls)
def test_cli_stop_cancels_service_restart_before_stopping_runtime(self):
from framebridge_expanded import cli
events = []
def stop_unit(state):
events.append("unit-stop")
def stop_runtime(state):
self.assertEqual(["unit-stop"], events)
events.append("runtime-stop")
return {"running": False}
with mock.patch.object(autostart, "stop_for_current_boot", side_effect=stop_unit), mock.patch.object(runtime, "stop", side_effect=stop_runtime), mock.patch.object(cli, "_show"):
self.assertEqual(0, cli.main(["--state-dir", str(self.state), "stop"]))
self.assertEqual(["unit-stop", "runtime-stop"], events)
+353
View File
@@ -0,0 +1,353 @@
import contextlib
import io
import os
from pathlib import Path
import tempfile
import unittest
from unittest import mock
from framebridge_expanded import cli
from framebridge_expanded.config import default_config, load_config, save_config
ROOT = Path(__file__).resolve().parents[1]
class CliTests(unittest.TestCase):
def directory(self):
return tempfile.TemporaryDirectory(prefix="cli-test-", dir=ROOT / "qa")
def answers(self, role="standalone", confirm="yes"):
def ask(prompt):
if prompt.startswith("Role "):
return role
if prompt.startswith("Computer name"):
return "expanded-test"
if prompt.startswith("ChatGPT email"):
return "tester@example.com"
if prompt.startswith("TLS certificate setup"):
return "existing"
if prompt.startswith("Central broker URL"):
return "https://central.example"
if prompt.startswith("Save this configuration?"):
return confirm
return ""
return ask
def test_help_has_english_commands_and_never_loads_runtime(self):
output = io.StringIO()
with contextlib.redirect_stdout(output), mock.patch.object(cli, "_module") as modules:
with self.assertRaises(SystemExit) as exited:
cli.main(["--help"])
self.assertEqual(exited.exception.code, 0)
self.assertIn("setup", output.getvalue())
self.assertIn("connect", output.getvalue())
modules.assert_not_called()
def test_no_argument_menu_uses_real_newlines(self):
output = io.StringIO()
with contextlib.redirect_stdout(output), mock.patch.object(cli.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", return_value="0"):
self.assertEqual(cli.main([]), 0)
self.assertIn("FrameBridge\n1. Connect\n2. Status\n3. Doctor", output.getvalue())
self.assertNotIn("\\n", output.getvalue())
def test_noninteractive_no_arguments_is_read_only_help(self):
with mock.patch.object(cli.sys.stdin, "isatty", return_value=False), mock.patch.object(cli, "_module") as modules, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(cli.main([]), 0)
modules.assert_not_called()
def test_state_environment_priority_and_alias(self):
with mock.patch.dict(os.environ, {"FRAMEBRIDGE_EXPANDED_STATE": "/tmp/canonical", "FRAMEBRIDGE_STATE_DIR": "/tmp/alias"}):
self.assertEqual(cli.resolve_state(), Path("/tmp/canonical"))
self.assertEqual(cli.resolve_state("/tmp/explicit"), Path("/tmp/explicit"))
with mock.patch.dict(os.environ, {}, clear=True):
os.environ["FRAMEBRIDGE_STATE_DIR"] = "/tmp/alias"
self.assertEqual(cli.resolve_state(), Path("/tmp/alias"))
def test_setup_roles_save_public_settings_without_start(self):
for role in ("standalone", "central", "node"):
with self.subTest(role=role), self.directory() as td, contextlib.redirect_stdout(io.StringIO()), mock.patch.object(cli, "_module") as modules:
state = Path(td) / "state"
self.assertEqual(cli.setup(state, ask=self.answers(role)), 0)
config = load_config(state)
self.assertEqual(config["role"], role)
self.assertEqual(config["account_label"], "tester@example.com")
self.assertEqual(config["ports"]["mcp"], 20865)
self.assertEqual((state / "config.json").stat().st_mode & 0o777, 0o600)
modules.assert_not_called()
serialized = (state / "config.json").read_text()
self.assertNotIn("api_key", serialized)
self.assertNotIn("runtime_key", serialized)
def test_setup_new_profile_selects_auto_without_a_desktop_question(self):
prompts = []
answer = self.answers()
def ask(prompt):
prompts.append(prompt)
return answer(prompt)
with self.directory() as td, contextlib.redirect_stdout(io.StringIO()) as output:
state = Path(td) / "state"
self.assertEqual(cli.setup(state, ask=ask), 0)
self.assertEqual(load_config(state)["headless"], "auto")
self.assertFalse(any("desktop" in prompt.lower() for prompt in prompts))
self.assertIn("Desktop: automatic detection.", output.getvalue())
def test_setup_keeps_existing_explicit_desktop_setting(self):
for mode in ("yes", "no", True, False):
with self.subTest(mode=mode), self.directory() as td, contextlib.redirect_stdout(io.StringIO()) as output:
state = Path(td) / "state"
settings = default_config(); settings["headless"] = mode
save_config(state, settings)
runtime = mock.Mock(); runtime.status.return_value = {"running": False, "processes": []}
with mock.patch.object(cli, "_module", return_value=runtime):
self.assertEqual(cli.setup(state, ask=self.answers()), 0)
self.assertEqual(load_config(state)["headless"], mode)
self.assertIn("setting preserved.", output.getvalue())
self.assertNotIn("Desktop: automatic detection.", output.getvalue())
def autostart_enable_output(self, result):
autostart = mock.Mock(); autostart.enable.return_value = result
with mock.patch.object(cli, "_module", return_value=autostart), contextlib.redirect_stdout(io.StringIO()) as output:
code = cli.main(["autostart", "enable"])
return code, output.getvalue()
def test_autostart_enable_distinguishes_configured_from_connected(self):
result = {"configured": True, "enabled": True, "service_state": "activating",
"runtime": {"ready": False, "display_context": {"kind": "text"}},
"connection": {"state": "checking", "verified": False}}
code, output = self.autostart_enable_output(result)
self.assertEqual(code, 0)
self.assertIn("Automatic startup enabled.", output)
self.assertIn("Tools: starting.", output)
self.assertIn("Desktop: text mode.", output)
self.assertIn("Connection: checking in the background.", output)
self.assertNotIn("Connection: ready.", output)
self.assertNotIn("CUA ready", output)
self.assertNotIn('"configured"', output)
def test_autostart_enable_reports_verified_graphical_capabilities(self):
result = {"configured": True, "enabled": True, "service_state": "active",
"runtime": {"ready": True, "display_context": {"kind": "physical-x11"},
"capabilities": {"fbexec": True, "fbterm": True, "cua_tty_module": True, "graphical_cua": True}},
"connection": {"state": "ready", "verified": True}}
code, output = self.autostart_enable_output(result)
self.assertEqual(code, 0)
self.assertIn("Tools: FBEXEC, FBTERM, CUA-TTY ready.", output)
self.assertIn("Desktop: detected; CUA ready.", output)
self.assertIn("Connection: ready.", output)
def test_autostart_enable_failed_service_is_not_a_success_exit(self):
code, output = self.autostart_enable_output({"configured": True, "enabled": True,
"service_state": "failed", "runtime": {"status_unavailable": True},
"connection": {"state": "error", "verified": False}})
self.assertEqual(code, 1)
self.assertIn("Service: failed", output)
self.assertIn("Tools: status unavailable.", output)
self.assertIn("Connection: needs attention.", output)
self.assertNotIn("CUA ready", output)
def test_autostart_enable_cancel_has_no_success_summary(self):
code, output = self.autostart_enable_output({"state": "cancelled"})
self.assertEqual(code, 130)
self.assertEqual(output, "Automatic startup was not enabled.\n")
def test_setup_decline_does_not_create_profile(self):
with self.directory() as td, contextlib.redirect_stdout(io.StringIO()):
state = Path(td) / "state"
self.assertEqual(cli.setup(state, ask=self.answers(confirm="no")), 0)
self.assertFalse((state / "config.json").exists())
def test_setup_rejects_running_profile_before_prompts(self):
with self.directory() as td:
state = Path(td)
save_config(state, default_config(name="existing"))
before = (state / "config.json").read_bytes()
runtime = mock.Mock()
runtime.status.return_value = {"running": True, "processes": []}
ask = mock.Mock()
with mock.patch.object(cli, "_module", return_value=runtime):
with self.assertRaisesRegex(RuntimeError, "Stop this profile"):
cli.setup(state, ask=ask)
ask.assert_not_called()
self.assertEqual((state / "config.json").read_bytes(), before)
def connection_runtime(self, base):
runtime = mock.Mock()
runtime.PREFIX = Path(base) / "payload"
binary = runtime.PREFIX / "original/geranas/FrameBridge-MCP-Private/bin/tunnel-client"
binary.parent.mkdir(parents=True)
binary.write_bytes(b"synthetic test binary")
runtime.status.return_value = {"running": False, "ready": False}
return runtime
def test_connection_rejects_another_profiles_listening_port(self):
with self.directory() as td:
state = Path(td) / "profile"
save_config(state, default_config(name="stopped-profile"))
before = (state / "config.json").read_bytes()
runtime = self.connection_runtime(td)
automatic = mock.Mock()
listener = mock.MagicMock()
listener.__enter__.return_value.bind.side_effect = OSError("address in use")
def connect(*args, **kwargs):
return kwargs["configure_connection"](state, {"account_label": "test@example.com", "tunnel_id": "tunnel_new"}, "SYNTHETIC-MCP-ACCESS-KEY")
with mock.patch.object(cli, "_module", side_effect=lambda name: runtime if name == "runtime" else automatic), mock.patch.object(cli.socket, "socket", return_value=listener), mock.patch.object(cli.tunnel, "connect_chatgpt", side_effect=connect):
with self.assertRaisesRegex(RuntimeError, "MCP port 20865 is already in use"):
cli.connect_chatgpt(state)
automatic.configure_connection.assert_not_called()
self.assertEqual(before, (state / "config.json").read_bytes())
def test_new_connection_needs_no_prior_profile_or_runtime_start(self):
with self.directory() as td:
state = Path(td) / "new-profile"
runtime = self.connection_runtime(td)
with mock.patch.object(cli, "_module", return_value=runtime), mock.patch.object(cli.tunnel, "connect_chatgpt", return_value=130) as connect:
self.assertEqual(130, cli.connect_chatgpt(state))
self.assertFalse(state.exists())
runtime.start.assert_not_called()
runtime.status.assert_not_called()
public = connect.call_args.args[3]
self.assertEqual("standalone", public["role"])
self.assertEqual("auto", public["headless"])
self.assertTrue(callable(connect.call_args.kwargs["configure_connection"]))
def test_cancel_new_profile_at_hidden_key_leaves_no_profile_or_services(self):
with self.directory() as td:
state = Path(td) / "new-profile"
runtime = self.connection_runtime(td)
automatic = mock.Mock()
with mock.patch.object(cli, "_module", side_effect=lambda name: runtime if name == "runtime" else automatic), mock.patch.object(cli.tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", return_value="test@example.com"), mock.patch.object(cli.tunnel, "_hidden", side_effect=["tunnel_valid", KeyboardInterrupt]), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(130, cli.connect_chatgpt(state))
self.assertFalse(state.exists())
automatic.configure_connection.assert_not_called()
runtime.start.assert_not_called()
def test_existing_managed_connection_dispatches_resume_without_wizard(self):
with self.directory() as td:
state = Path(td) / "profile"
save_config(state, default_config())
(state / "autostart").mkdir()
(state / "autostart/installation.json").write_text("{}")
runtime = self.connection_runtime(td)
automatic = mock.Mock()
with mock.patch.object(cli, "_module", side_effect=lambda name: runtime if name == "runtime" else automatic), mock.patch.object(cli.tunnel, "resume_managed_connection", return_value=0) as resume, mock.patch.object(cli.tunnel, "connect_chatgpt") as wizard:
self.assertEqual(0, cli.connect_chatgpt(state))
resume.assert_called_once_with(state, automatic.resume_connection)
wizard.assert_not_called()
def test_replace_bypasses_resume_and_uses_normal_three_field_wizard(self):
with self.directory() as td:
state = Path(td) / "profile"
save_config(state, default_config())
(state / "autostart").mkdir()
(state / "autostart/installation.json").write_text("{}")
runtime = self.connection_runtime(td)
with mock.patch.object(cli, "_module", return_value=runtime), mock.patch.object(cli.tunnel, "resume_managed_connection") as resume, mock.patch.object(cli.tunnel, "connect_chatgpt", return_value=0) as wizard:
self.assertEqual(0, cli.connect_chatgpt(state, replace=True))
resume.assert_not_called()
self.assertTrue(callable(wizard.call_args.kwargs["configure_connection"]))
def test_replace_dispatch_and_foreground_are_mutually_exclusive(self):
with contextlib.redirect_stdout(io.StringIO()), mock.patch.object(cli, "connect_chatgpt", return_value=0) as connect:
self.assertEqual(0, cli.main(["--state-dir", "/tmp/profile", "connect", "chatgpt", "--replace"]))
connect.assert_called_once_with(Path("/tmp/profile"), foreground=False, replace=True)
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error:
cli.main(["connect", "chatgpt", "--replace", "--foreground"])
self.assertEqual(2, error.exception.code)
def test_node_profile_cannot_tunnel_a_central_profiles_port(self):
with self.directory() as td:
state = Path(td)
save_config(state, default_config("node", "node-profile"))
with mock.patch.object(cli, "_module") as modules, mock.patch.object(cli.tunnel, "connect_chatgpt") as connect:
with self.assertRaisesRegex(RuntimeError, "central profile"):
cli.connect_chatgpt(state)
modules.assert_not_called()
connect.assert_not_called()
def test_lifecycle_dispatch_receives_explicit_profile(self):
runtime = mock.Mock()
runtime.status.return_value = {"ok": True, "running": False}
with contextlib.redirect_stdout(io.StringIO()), mock.patch.object(cli, "_module", return_value=runtime):
self.assertEqual(cli.main(["--state-dir", str(ROOT / "qa/example"), "status"]), 0)
runtime.status.assert_called_once_with(ROOT / "qa/example")
def test_tty_dispatch_uses_authorized_typed_payload(self):
tty = mock.Mock()
tty.request.return_value = {"ok": True}
with contextlib.redirect_stdout(io.StringIO()), mock.patch.object(cli, "_module", return_value=tty):
self.assertEqual(cli.main(["--state-dir", str(ROOT / "qa/example"), "tty", "key", "session-1", "Enter"]), 0)
tty.request.assert_called_once_with(ROOT / "qa/example", {"authorized": True, "op": "key.send", "session_id": "session-1", "key": "Enter"})
def test_node_enrollment_does_not_start_agent(self):
with self.directory() as td:
state = Path(td)
config = default_config("node", "node-test")
config["broker_url"] = "https://central.example"
save_config(state, config)
node = mock.Mock()
node.enroll.return_value = {"ok": True, "state": "pending"}
with contextlib.redirect_stdout(io.StringIO()), mock.patch.object(cli, "_module", return_value=node):
self.assertEqual(cli.main(["--state-dir", str(state), "nodes", "enroll"]), 0)
node.enroll.assert_called_once_with("https://central.example", "node-test", state_dir=state)
node.run.assert_not_called()
def test_nodes_list_scopes_and_restores_environment(self):
with self.directory() as td:
state = Path(td)
save_config(state, default_config(name="list-test"))
broker = mock.Mock()
fleet = mock.Mock()
def discover():
self.assertEqual(os.environ["FRAMEBRIDGE_EXPANDED_STATE"], str(state))
return {"ok": True, "nodes": []}
fleet.discover_nodes.side_effect = discover
before = os.environ.get("FRAMEBRIDGE_EXPANDED_STATE")
with contextlib.redirect_stdout(io.StringIO()), mock.patch.object(cli, "_module", side_effect=lambda name: broker if name == "broker" else fleet):
self.assertEqual(cli.main(["--state-dir", str(state), "nodes", "list"]), 0)
broker.configure.assert_called_once_with(None, state_dir=state)
self.assertEqual(os.environ.get("FRAMEBRIDGE_EXPANDED_STATE"), before)
def test_nodes_approval_requires_expected_uuid(self):
parser = cli.build_parser()
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
parser.parse_args(["nodes", "approve", "request-1"])
def test_central_wizard_creates_lan_tls_only_after_review(self):
with self.directory() as td, contextlib.redirect_stdout(io.StringIO()) as output:
state = Path(td) / "state"
original = self.answers("central")
def ask(prompt):
if prompt.startswith("Broker listen host"):
return "0.0.0.0"
if prompt.startswith("TLS certificate setup"):
return "generate"
if prompt.startswith("Certificate DNS name"):
return "central.example.test"
if prompt.startswith("Advertised broker IP address"):
return "192.0.2.20"
return original(prompt)
self.assertEqual(cli.setup(state, ask=ask), 0)
config = load_config(state)
self.assertEqual(config["broker_url"], "https://192.0.2.20:20878")
self.assertEqual(config["broker_control_url"], "https://127.0.0.1:20878")
self.assertEqual(config["broker_listen_host"], "0.0.0.0")
self.assertTrue(Path(config["tls_ca"]).is_file())
self.assertFalse((state / "manager.json").exists())
self.assertIn("No service or ChatGPT connection was started.", output.getvalue())
self.assertNotIn("PRIVATE KEY", output.getvalue())
def test_certificates_create_dispatch_uses_explicit_profile(self):
with self.directory() as td, contextlib.redirect_stdout(io.StringIO()) as output:
state = Path(td)
save_config(state, default_config("central", "cert-cli"))
self.assertEqual(cli.main(["--state-dir", str(state), "certificates", "create",
"--host", "cert.example.test", "--address", "192.0.2.30",
"--listen-host", "0.0.0.0"]), 0)
self.assertEqual(load_config(state)["broker_url"], "https://192.0.2.30:20878")
self.assertIn("ca_fingerprint_sha256", output.getvalue())
self.assertNotIn("PRIVATE KEY", output.getvalue())
if __name__ == "__main__":
unittest.main()
+232
View File
@@ -0,0 +1,232 @@
"""Command contract tests: mocked interfaces only, no external connections."""
import contextlib
import io
import json
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import cli
from framebridge_expanded.config import default_config, save_config
class CommunityCliContractTests(unittest.TestCase):
def call(self, args, modules=None):
stdout, stderr = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
if modules is None:
code = cli.main(args)
else:
with mock.patch.object(cli, "_module", side_effect=lambda name: modules[name]):
code = cli.main(args)
return code, stdout.getvalue(), stderr.getvalue()
def aggregate(self):
return {"ok": True, "runtime":{"role":"standalone", "ready":True, "running":True},
"connection":{"ok":True,"verified":False,"state":"closed"},
"boot":{"configured":True,"enabled":True,"linger":True,"encrypted_credential_present":True,
"service_active":True,"service_state":"active"},
"desired_connection":"disconnected"}
def test_short_and_legacy_connect_share_flags(self):
for args in (["connect", "--replace"], ["connect", "chatgpt", "--replace"]):
value=cli.build_parser().parse_args(args)
self.assertTrue(value.replace)
self.assertFalse(value.foreground)
def test_common_json_and_profile_options_survive_nested_parsers(self):
for args in (["--json","--state-dir","/qa/custom","service","boot","disable"],
["service","boot","disable","--json","--state-dir","/qa/custom"]):
value=cli.build_parser().parse_args(args)
self.assertTrue(value.json)
self.assertEqual(value.state_dir,"/qa/custom")
self.assertEqual(value.boot_action,"disable")
def test_plain_status_separates_live_connection_and_boot(self):
life=SimpleNamespace(status=mock.Mock(return_value=self.aggregate()))
code,text,_=self.call(["status"],{"lifecycle":life})
self.assertEqual(code,0)
self.assertIn("Local tools: ready",text)
self.assertIn("ChatGPT connection: closed",text)
self.assertIn("Automatic startup: enabled",text)
self.assertNotIn("connected (verified)",text)
def test_unavailable_status_is_not_presented_as_stopped_or_disabled(self):
value={"ok":True,"runtime":{"status_unavailable":True},"connection":{"state":"unavailable"},"boot":{},"desired_connection":"unavailable"}
life=SimpleNamespace(status=mock.Mock(return_value=value))
code,text,_=self.call(["status"],{"lifecycle":life})
self.assertEqual(code,0)
self.assertIn("Local tools: unavailable",text)
self.assertIn("Automatic startup: unavailable",text)
self.assertNotIn("Local tools: stopped",text)
def test_json_status_is_one_versioned_object(self):
life=SimpleNamespace(status=mock.Mock(return_value=self.aggregate()))
code,text,_=self.call(["status","--json"],{"lifecycle":life})
value=json.loads(text)
self.assertEqual(code,0)
self.assertEqual(value["schema_version"],1)
self.assertEqual(value["version"],"0.3.1")
self.assertEqual(value["result"]["desired_connection"],"disconnected")
self.assertFalse(value["result"]["connection"]["verified"])
def test_scoped_status_uses_same_model_and_includes_intent(self):
life=SimpleNamespace(status=mock.Mock(return_value=self.aggregate()))
code,text,_=self.call(["--json","status","connection"],{"lifecycle":life})
data=json.loads(text)["result"]
self.assertEqual(code,0)
self.assertEqual(data["state"],"closed")
self.assertEqual(data["desired_connection"],"disconnected")
self.assertNotIn("runtime",data)
def test_doctor_is_distinct_and_does_not_use_status(self):
runtime=SimpleNamespace(doctor=mock.Mock(return_value={"ok":False,"compatible_debian_13_amd64":True,
"display":"text tools","missing_commands":["tmux"],"issues":[]}))
code,text,_=self.call(["doctor"],{"runtime":runtime})
self.assertEqual(code,1)
self.assertIn("Missing command: tmux",text)
self.assertIn("use fb status",text)
def test_json_new_connect_never_prompts_or_starts_services(self):
with tempfile.TemporaryDirectory() as td, mock.patch("builtins.input") as ask, \
mock.patch.object(cli,"connect_chatgpt") as connect, mock.patch.object(cli,"_module") as modules:
code,text,_=self.call(["--state-dir",td,"connect","--json"])
self.assertEqual(code,2)
self.assertEqual(json.loads(text)["result"]["error"],"INTERACTION_REQUIRED")
ask.assert_not_called();connect.assert_not_called();modules.assert_not_called()
def test_structured_foreground_next_action_preserves_temporary_mode(self):
with tempfile.TemporaryDirectory() as td,mock.patch.object(cli,"connect_chatgpt") as connect:
code,text,_=self.call(["--state-dir",td,"connect","--foreground","--json"])
self.assertEqual(code,2)
self.assertTrue(json.loads(text)["result"]["next_action"].endswith(" connect --foreground"))
connect.assert_not_called()
def test_menu_cancel_returns_cleanly_without_dispatch(self):
with mock.patch.object(cli.sys.stdin,"isatty",return_value=True),mock.patch("builtins.input",side_effect=KeyboardInterrupt):
code,text,errors=self.call([])
self.assertEqual(code,130)
self.assertIn("Cancelled.",errors)
def test_json_setup_reports_interaction_without_reading_prompts(self):
with mock.patch("builtins.input") as ask,mock.patch.object(cli,"setup") as setup:
code,text,_=self.call(["setup","--json"])
self.assertEqual(code,2)
self.assertEqual(json.loads(text)["result"]["error"],"INTERACTION_REQUIRED")
ask.assert_not_called();setup.assert_not_called()
def test_saved_json_connect_keeps_human_progress_off_stdout(self):
life=SimpleNamespace(status=mock.Mock(return_value=self.aggregate()))
with tempfile.TemporaryDirectory() as td:
state=Path(td);(state/"autostart").mkdir();(state/"autostart/installation.json").write_text("{}")
def connect(*args,**kwargs):
print("Checking the saved connection.")
return 0
with mock.patch.object(cli,"connect_chatgpt",side_effect=connect):
code,text,errors=self.call(["--state-dir",td,"connect","--json"],{"lifecycle":life})
self.assertEqual(code,0)
self.assertEqual(json.loads(text)["result"]["runtime"]["ready"],True)
self.assertIn("Checking the saved connection.",errors)
self.assertNotIn("Checking",text)
def test_canonical_disconnect_and_legacy_have_explicit_different_routes(self):
canonical=SimpleNamespace(disconnect=mock.Mock(return_value={"ok":True}))
legacy=SimpleNamespace(disconnect=mock.Mock(return_value={"ok":True}))
modules={"lifecycle":canonical,"connection_background":legacy}
self.assertEqual(self.call(["disconnect"],modules)[0],0)
canonical.disconnect.assert_called_once();legacy.disconnect.assert_not_called()
self.assertEqual(self.call(["disconnect","chatgpt"],modules)[0],0)
legacy.disconnect.assert_called_once()
def test_noninteractive_service_start_does_not_invent_authentication_gate(self):
life=SimpleNamespace(service_start=mock.Mock(return_value={"ok":True,"state":"started","scope":"local_tools"}))
code,text,_=self.call(["service","start","--json"],{"lifecycle":life})
self.assertEqual(code,0)
self.assertEqual(json.loads(text)["result"]["scope"],"local_tools")
life.service_start.assert_called_once()
def test_first_node_service_start_reports_interaction_before_provisioning(self):
life=SimpleNamespace(status=mock.Mock(return_value={"boot":{"configured":False}}),service_start=mock.Mock())
with tempfile.TemporaryDirectory() as td:
state=Path(td);config=default_config();config["role"]="node";save_config(state,config)
code,text,_=self.call(["--state-dir",td,"service","start","--json"],{"lifecycle":life})
self.assertEqual(code,2)
self.assertEqual(json.loads(text)["result"]["error"],"INTERACTION_REQUIRED")
life.service_start.assert_not_called()
def test_configured_node_service_start_remains_noninteractive(self):
life=SimpleNamespace(status=mock.Mock(return_value={"boot":{"configured":True}}),service_start=mock.Mock(return_value={"ok":True,"state":"start_requested"}))
with tempfile.TemporaryDirectory() as td:
state=Path(td);config=default_config();config["role"]="node";save_config(state,config)
code,text,_=self.call(["--state-dir",td,"service","start","--json"],{"lifecycle":life})
self.assertEqual(code,0)
self.assertEqual(json.loads(text)["result"]["state"],"start_requested")
life.service_start.assert_called_once()
def test_boot_disable_and_legacy_autostart_disable_are_not_retargeted(self):
life=SimpleNamespace(boot_disable=mock.Mock(return_value={"ok":True,"state":"boot-disabled"}))
auto=SimpleNamespace(disable=mock.Mock(return_value={"ok":True,"state":"disabled"}),
status=mock.Mock(return_value={"service_state":"inactive"}))
modules={"lifecycle":life,"autostart":auto}
self.call(["service","boot","disable"],modules)
life.boot_disable.assert_called_once();auto.disable.assert_not_called()
self.call(["autostart","disable"],modules)
auto.disable.assert_called_once()
def test_forget_requires_explicit_confirmation_without_calling_destructive_api(self):
life=SimpleNamespace(forget_credentials=mock.Mock())
code,text,_=self.call(["service","credentials","forget","--json"],{"lifecycle":life})
self.assertEqual(code,2)
self.assertEqual(json.loads(text)["result"]["error"],"CONFIRMATION_REQUIRED")
life.forget_credentials.assert_not_called()
def test_forget_confirmation_instruction_preserves_custom_profile(self):
life=SimpleNamespace(forget_credentials=mock.Mock())
code,text,_=self.call(["--state-dir","/qa/profile with spaces","service","credentials","forget","--json"],{"lifecycle":life})
self.assertEqual(code,2)
self.assertEqual(json.loads(text)["result"]["next_action"],"fb --state-dir '/qa/profile with spaces' service credentials forget --confirm")
life.forget_credentials.assert_not_called()
def test_explicit_confirm_forget_calls_one_narrow_api(self):
life=SimpleNamespace(forget_credentials=mock.Mock(return_value={"ok":True,"state":"forgotten"}))
self.assertEqual(self.call(["service","credentials","forget","--confirm","--json"],{"lifecycle":life})[0],0)
life.forget_credentials.assert_called_once()
def test_console_login_subcommand_calls_only_login_policy(self):
startup=SimpleNamespace(status=mock.Mock(return_value={"ok":True,"enabled":False}))
code,text,_=self.call(["console","login","status","--json"],{"console_startup":startup})
self.assertEqual(code,0)
self.assertFalse(json.loads(text)["result"]["enabled"])
startup.status.assert_called_once()
def test_terminal_service_group_routes_same_explicit_target(self):
args=cli.build_parser().parse_args(["tty","service","status","--node","N2"])
with mock.patch.object(cli,"_target_tty",return_value={"ok":True}) as target,contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(cli.tty_command(args,Path("/qa/profile")),0)
target.assert_called_once_with(Path("/qa/profile"),"N2",service="status")
def test_noninteractive_typed_text_without_text_never_prompts(self):
with mock.patch("builtins.input") as ask,mock.patch.object(cli,"tty_command") as tty:
code,text,_=self.call(["tty","type","a"*32,"--json"])
self.assertEqual(code,2)
self.assertEqual(json.loads(text)["result"]["error"],"INTERACTION_REQUIRED")
ask.assert_not_called();tty.assert_not_called()
def test_help_uses_same_versioned_guide_without_runtime(self):
guide=SimpleNamespace(get_guide=mock.Mock(return_value={"ok":True,"topic":"commands","text":"Command guide"}))
code,text,_=self.call(["help","commands","--json"],{"guide":guide})
self.assertEqual(code,0)
self.assertEqual(json.loads(text)["result"]["topic"],"commands")
guide.get_guide.assert_called_once_with("commands")
def test_failed_pause_does_not_claim_success(self):
life=SimpleNamespace(disconnect=mock.Mock(return_value={"ok":False,"error":"TEST_FAILURE"}))
code,text,errors=self.call(["disconnect"],{"lifecycle":life})
self.assertEqual(code,1)
self.assertNotIn("paused until",text)
self.assertIn("TEST_FAILURE",errors)
if __name__=="__main__":
unittest.main()
+98
View File
@@ -0,0 +1,98 @@
"""Routing regression tests for package CLI additions; no external services."""
import contextlib
import io
import os
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import cli
class CliExtensionTests(unittest.TestCase):
SID = "c" * 32
def parsed(self, *parts):
return cli.build_parser().parse_args(list(parts))
def test_sudo_parser_keeps_node_and_console_variables(self):
for mode in ("on", "status", "off"):
args = self.parsed("tty", "sudo", mode, self.SID, "--node", "NodeCase")
self.assertEqual((args.tty_command, args.sudo_mode, args.session_id, args.node),
("sudo", mode, self.SID, "NodeCase"))
def test_sudo_command_routes_exact_session_and_target(self):
args = self.parsed("tty", "sudo", "on", self.SID, "--node", "NodeCase")
with mock.patch.object(cli, "_target_tty", return_value={"ok": True}) as target, \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(cli.tty_command(args, Path("/qa/profile")), 0)
target.assert_called_once_with(Path("/qa/profile"), "NodeCase",
payload={"authorized": True, "op": "sudo", "session_id": self.SID, "mode": "on"})
def test_remote_request_uses_profile_and_existing_fleet_target(self):
seen = []
def execute_node(node, **kwargs):
seen.append((node, os.environ.get("FRAMEBRIDGE_EXPANDED_STATE"), kwargs))
return {"ok": True}
def request(execute, target, payload):
return execute(target=target, script="printf bounded", authorized=True,
timeout_s=30, job_id="qa", sha256="d" * 64, include_stdout=True)
modules = {
"fleet": SimpleNamespace(execute_node=execute_node),
"tty_mcp": SimpleNamespace(_remote_request=request),
}
with mock.patch.dict(os.environ, {"FRAMEBRIDGE_EXPANDED_STATE": "/previous/profile"}), \
mock.patch.object(cli, "_module", side_effect=lambda name: modules[name]):
value = cli._target_tty(Path("/qa/explicit"), "NodeCase", payload={"op": "observe"})
self.assertEqual(os.environ["FRAMEBRIDGE_EXPANDED_STATE"], "/previous/profile")
self.assertTrue(value["ok"])
self.assertEqual(seen[0][:2], ("NodeCase", "/qa/explicit"))
self.assertNotIn("target", seen[0][2])
def test_remote_transport_failure_restores_absent_profile_without_local_fallback(self):
modules = {
"fleet": SimpleNamespace(execute_node=mock.Mock()),
"tty_mcp": SimpleNamespace(_remote_request=mock.Mock(side_effect=RuntimeError("transport failed"))),
}
with mock.patch.dict(os.environ, {}, clear=True), \
mock.patch.object(cli, "_module", side_effect=lambda name: modules[name]):
with self.assertRaisesRegex(RuntimeError, "transport failed"):
cli._target_tty(Path("/qa/explicit"), "NodeCase", payload={"op": "status"})
self.assertNotIn("FRAMEBRIDGE_EXPANDED_STATE", os.environ)
modules["fleet"].execute_node.assert_not_called()
def test_remote_service_action_uses_wire_cli_instead_of_local_daemon(self):
seen = []
def wire(execute, target, arguments):
seen.append((target, arguments, os.environ.get("FRAMEBRIDGE_EXPANDED_STATE")))
return {"ok": True}
modules = {
"fleet": SimpleNamespace(execute_node=mock.Mock()),
"tty_mcp": SimpleNamespace(_exec_json=wire),
}
with mock.patch.object(cli, "_module", side_effect=lambda name: modules[name]):
cli._target_tty(Path("/qa/profile"), "NodeCase", service="status")
self.assertEqual(seen, [("NodeCase", ["status"], "/qa/profile")])
def test_local_request_keeps_explicit_profile(self):
request = mock.Mock(return_value={"ok": True})
with mock.patch.object(cli, "_module", return_value=SimpleNamespace(request=request)):
cli._target_tty(Path("/qa/profile"), "LOCAL", payload={"op": "status"})
request.assert_called_once_with(Path("/qa/profile"), {"op": "status"})
def test_failed_target_result_is_not_reported_as_success(self):
args = self.parsed("tty", "close", self.SID, "--node", "NodeCase")
with mock.patch.object(cli, "_target_tty", return_value={"ok": False, "error": "REMOTE_FAILED"}), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(cli.tty_command(args, Path("/qa/profile")), 1)
def test_text_transcript_observe_retains_node_argument(self):
args = self.parsed("tty", "observe", self.SID, "--node", "NodeCase")
with mock.patch.object(cli, "_target_tty", return_value={"ok": True}) as target, \
contextlib.redirect_stdout(io.StringIO()):
cli.tty_command(args, Path("/qa/profile"))
self.assertEqual(target.call_args.kwargs["payload"]["op"], "observe")
self.assertEqual(target.call_args.args[1], "NodeCase")
if __name__ == "__main__":
unittest.main()
+204
View File
@@ -0,0 +1,204 @@
import io
import json
import os
from pathlib import Path
import pty
import signal
import subprocess
import sys
import tempfile
import time
import unittest
from unittest import mock
from framebridge_expanded import connection_background as background
from framebridge_expanded import tunnel
ID = "tunnel_background_fixture"
MCP = "http://127.0.0.1:20865/mcp"
FAKE = "SYNTHETIC_BACKGROUND_ONLY_0123456789"
CLIENT = r'''
import datetime,http.server,json,os,sys,time
from pathlib import Path
started=datetime.datetime.now(datetime.timezone.utc).isoformat()
identifier=sys.argv[sys.argv.index('--control-plane.tunnel-id')+1]
mcp=sys.argv[sys.argv.index('--mcp.server-url')+1].removeprefix('url=')
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_GET(self):
if self.path=='/readyz': body=b'ready'
elif self.path=='/healthz': body=b'live'
elif self.path=='/api/status': body=json.dumps({'control_plane_tunnel_id':identifier,'mcp_server_url':mcp,'started_at':started,'tunnel_metadata':{'ID':identifier}}).encode()
elif self.path=='/metrics': body=('commands_poll_last_successful_timestamp_seconds '+str(time.time())+'\n').encode()
else: self.send_response(404);self.end_headers();return
self.send_response(200);self.end_headers();self.wfile.write(body)
server=http.server.HTTPServer(('127.0.0.1',0),Handler)
Path(sys.argv[sys.argv.index('--health.url-file')+1]).write_text('http://127.0.0.1:'+str(server.server_port))
print('TECHNICAL_FIXTURE '+os.environ['FRAMEBRIDGE_EXPANDED_RUNTIME_KEY'],flush=True)
server.serve_forever()
'''
def command(code=CLIENT):
return [sys.executable, "-u", "-c", code, "--control-plane.tunnel-id", ID,
"--mcp.server-url", "url=" + MCP]
class BackgroundTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="fb-background-test-")
self.state = Path(self.temp.name)
def tearDown(self):
background.disconnect(self.state)
self.temp.cleanup()
def start(self, wait=10):
output = io.StringIO()
rc = background.start(command(), FAKE, self.state, output=output, wait_timeout=wait)
self.assertEqual(rc, 0, output.getvalue())
return output.getvalue()
def test_real_detached_worker_ready_private_logs_and_no_credential_on_disk(self):
output = self.start()
current = background.status(self.state)
self.assertTrue(current["alive"])
self.assertTrue(current["verified"])
self.assertIn("terminal can be closed", output)
self.assertNotIn(FAKE, output)
self.assertNotIn("TECHNICAL_FIXTURE", output)
worker = Path('/proc') / str(current['pid'])
self.assertNotIn(FAKE.encode(), (worker / 'cmdline').read_bytes())
self.assertNotIn(FAKE.encode(), (worker / 'environ').read_bytes())
log = self.state / 'logs/chatgpt/connection.log'
deadline = time.monotonic()+3
while 'TECHNICAL_FIXTURE' not in log.read_text() and time.monotonic()<deadline:
time.sleep(.05)
self.assertIn('TECHNICAL_FIXTURE [REDACTED]', log.read_text())
for path in self.state.rglob('*'):
if path.is_file():
self.assertNotIn(FAKE.encode(), path.read_bytes(), str(path))
self.assertEqual((self.state / 'connection').stat().st_mode & 0o777, 0o700)
for name in ('owner.lock','status.json'):
self.assertEqual((self.state / 'connection' / name).stat().st_mode & 0o777, 0o600)
stopped = background.disconnect(self.state)
self.assertTrue(stopped['ok'])
self.assertFalse(background.status(self.state)['alive'])
self.assertTrue(background.disconnect(self.state)['ok'])
def test_second_connect_reuses_owned_profile_without_duplicate_worker(self):
self.start()
pid = background.status(self.state)['pid']
output = io.StringIO()
self.assertEqual(background.start(command(), FAKE, self.state, output=output, wait_timeout=2),0)
self.assertEqual(background.status(self.state)['pid'],pid)
self.assertIn('already owns this profile',output.getvalue())
def test_startup_failure_never_reports_ready_and_releases_lock(self):
output = io.StringIO()
rc = background.start(command('raise SystemExit(7)'), FAKE, self.state, output=output, wait_timeout=5)
self.assertEqual(rc,1)
self.assertNotIn('[OK]',output.getvalue())
self.assertFalse(background.status(self.state)['alive'])
self.start()
def test_pending_is_explicit_and_keeps_background_worker(self):
output=io.StringIO()
rc=background.start(command('import time;time.sleep(60)'),FAKE,self.state,output=output,wait_timeout=.7)
self.assertEqual(rc,1)
self.assertIn('continues retrying in the background',output.getvalue())
self.assertTrue(background.status(self.state)['alive'])
self.assertFalse(background.status(self.state)['verified'])
def test_closed_controlling_terminal_does_not_stop_verified_tunnel(self):
master,slave=pty.openpty()
root=Path(background.__file__).resolve().parents[1]
code=("import os,fcntl,termios,sys,signal;os.setsid();fcntl.ioctl(0,termios.TIOCSCTTY,0);"
+"sys.path.insert(0,"+repr(str(root))+");from framebridge_expanded import connection_background as b;"
+"rc=b.start("+repr(command())+","+repr(FAKE)+","+repr(str(self.state))+",wait_timeout=10);"
+"signal.pause()")
parent=subprocess.Popen([sys.executable,'-c',code],stdin=slave,stdout=slave,stderr=slave,close_fds=True)
os.close(slave)
try:
deadline=time.monotonic()+12
while time.monotonic()<deadline and not background.status(self.state)['verified']:
self.assertIsNone(parent.poll())
time.sleep(.1)
self.assertTrue(background.status(self.state)['verified'])
os.close(master);master=None
parent.wait(timeout=3)
time.sleep(.3)
self.assertTrue(background.status(self.state)['verified'])
finally:
if master is not None: os.close(master)
if parent.poll() is None: parent.terminate();parent.wait(timeout=3)
def test_stale_heartbeat_is_not_verified_and_forged_message_is_not_printed(self):
self.start()
pid=background.status(self.state)['pid']
os.kill(pid,signal.SIGSTOP)
try:
path=self.state/'connection/status.json'
record=json.loads(path.read_text());record['verified_at']=time.time()-30
record['message']='\x1b[31m'+FAKE
background._write(path,record)
current=background.status(self.state)
self.assertFalse(current['verified'])
self.assertEqual(current['state'],'retrying')
self.assertNotIn(FAKE,current['message'])
self.assertNotIn('\x1b',current['message'])
finally:
os.kill(pid,signal.SIGCONT)
def test_forged_identity_does_not_signal_unrelated_process(self):
child=subprocess.Popen([sys.executable,'-c','import time;time.sleep(30)'],start_new_session=True)
try:
directory=background._directory(self.state)
lock=background._checked_fd(directory/'owner.lock',os.O_RDWR,create=True)
try:
record=background._identity(child.pid,self.state,lock)
record.update(state='ready',verified_at=time.time())
background._write(directory/'status.json',record)
with mock.patch.object(signal,'pidfd_send_signal') as send:
self.assertTrue(background.disconnect(self.state)['ok'])
send.assert_not_called()
self.assertIsNone(child.poll())
self.assertFalse(background.status(self.state)['verified'])
finally: os.close(lock)
finally:
child.terminate();child.wait(timeout=3)
def test_foreground_mode_cannot_overlap_owned_background_connection(self):
self.start()
with self.assertRaisesRegex(RuntimeError, 'already owns'):
background.run_foreground(command(),FAKE,self.state,output=io.StringIO())
self.assertTrue(background.status(self.state)['verified'])
def test_ctrl_c_before_published_state_reaps_owned_supervisor(self):
real_spawn=subprocess.Popen
seen=[]
def spawn(*args,**kwargs):
child=real_spawn(*args,**kwargs)
seen.append(child)
return child
real_sleep=time.sleep
interrupted=[]
def interrupt_once(seconds):
if not interrupted:
interrupted.append(True)
raise KeyboardInterrupt
return real_sleep(seconds)
with mock.patch.object(background.subprocess,'Popen',side_effect=spawn), mock.patch.object(background.time,'sleep',side_effect=interrupt_once):
rc=background.start(command(),FAKE,self.state,output=io.StringIO(),wait_timeout=5)
self.assertEqual(rc,130)
self.assertTrue(seen)
self.assertIsNotNone(seen[0].poll())
self.assertFalse(background.status(self.state)['alive'])
def test_interactive_background_does_not_prompt_when_already_running(self):
self.start()
with mock.patch.object(tunnel,'_hidden') as prompt:
self.assertEqual(tunnel.connect_chatgpt('/unused',self.state,MCP,output=io.StringIO()),0)
prompt.assert_not_called()
if __name__=='__main__': unittest.main()
+280
View File
@@ -0,0 +1,280 @@
import datetime
import http.server
import io
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import threading
import time
import unittest
from unittest import mock
from framebridge_expanded import connection_monitor as monitor
from framebridge_expanded import tunnel
ID = "tunnel_synthetic_connection_test"
MCP = "http://127.0.0.1:20865/mcp"
class HTTPFixture:
def __init__(self, root):
self.started = time.time()
self.ready = "ready"
self.ready_code = 200
self.metadata_error = ""
self.poll = self.started
self.tunnel = ID
self.mcp = MCP
self.hits = []
fixture = self
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
fixture.hits.append(self.path)
if self.path == "/readyz":
code, data = fixture.ready_code, fixture.ready.encode()
elif self.path == "/api/status":
code = 200
data = json.dumps({"control_plane_tunnel_id": fixture.tunnel,
"mcp_server_url": fixture.mcp,
"started_at": datetime.datetime.fromtimestamp(fixture.started, datetime.timezone.utc).isoformat(),
"tunnel_metadata": {"ID": fixture.tunnel},
"tunnel_metadata_error": fixture.metadata_error}).encode()
else:
code, data = 200, ("commands_poll_last_successful_timestamp_seconds " + str(fixture.poll) + "\n").encode()
self.send_response(code)
if code == 302:
self.send_header("Location", "http://192.0.2.1/forbidden")
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
self.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
self.file = Path(root) / "health.url"
self.file.write_text("http://127.0.0.1:" + str(self.server.server_port))
def probe(self):
return monitor.probe(self.file, ID, MCP, self.started)
def close(self):
self.server.shutdown()
self.server.server_close()
self.thread.join()
class ReadinessTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="fb-health-test-")
self.fixture = HTTPFixture(self.temp.name)
self.addCleanup(self.temp.cleanup)
self.addCleanup(self.fixture.close)
def test_ready_requires_local_mcp_identity_and_authenticated_poll(self):
self.assertEqual(self.fixture.probe()[0], "ready")
self.assertEqual(self.fixture.hits, ["/readyz", "/api/status", "/metrics"])
def test_no_success_for_live_client_without_successful_poll(self):
self.fixture.poll = 0
self.assertEqual(self.fixture.probe()[0], "access")
def test_no_success_for_stale_or_future_poll(self):
self.fixture.poll = self.fixture.started - 200
self.assertEqual(self.fixture.probe()[0], "access")
self.fixture.poll = self.fixture.started + 200
self.assertEqual(self.fixture.probe()[0], "access")
def test_partial_ready_auth_and_timeout_are_not_full_ready(self):
for suffix in [" (mcp initialize requires auth: synthetic)", " (mcp startup probe timed out: synthetic)"]:
self.fixture.ready = "ready" + suffix
self.assertEqual(self.fixture.probe()[0], "checking")
self.assertNotIn("/metrics", self.fixture.hits)
def test_changed_identity_or_old_startup_not_accepted(self):
self.fixture.tunnel = "tunnel_other"
self.assertEqual(self.fixture.probe()[0], "checking")
self.fixture.tunnel = ID
self.assertEqual(monitor.probe(self.fixture.file, ID, MCP, self.fixture.started + 50)[0], "checking")
def test_metadata_error_is_not_connected(self):
self.fixture.metadata_error = "HTTP 403 synthetic fixture"
self.assertEqual(self.fixture.probe()[0], "rejected")
def test_redirects_and_proxy_environment_do_not_redirect_probe(self):
self.fixture.ready_code = 302
with mock.patch.dict(os.environ, {"HTTP_PROXY": "http://192.0.2.2:1", "NO_PROXY": ""}):
self.assertEqual(self.fixture.probe()[0], "checking")
self.assertEqual(self.fixture.hits, ["/readyz"])
def test_health_file_rejects_remote_credentials_query_and_symlink(self):
for value in ["http://192.0.2.1:8", "http://user:pw@127.0.0.1:8", "http://127.0.0.1:8/?token=x", "http://localhost:8", "https://127.0.0.1:8"]:
self.fixture.file.write_text(value)
with self.assertRaises(monitor.ConnectionMonitorError):
monitor.health_endpoint(self.fixture.file)
self.fixture.file.unlink()
self.fixture.file.symlink_to(Path(self.temp.name) / "missing")
with self.assertRaises(OSError):
monitor.health_endpoint(self.fixture.file)
class PrivateLogTests(unittest.TestCase):
def test_rotation_is_bounded_and_preserves_only_redacted_private_records(self):
with tempfile.TemporaryDirectory() as td:
log = monitor.PrivateLog(td, "SYNTHETIC_SECRET", max_bytes=220)
for _ in range(20):
log.write("event SYNTHETIC_SECRET " + "x" * 55)
log.close()
files = list((Path(td) / "logs/chatgpt").glob("connection.log*"))
self.assertLessEqual(len(files), 3)
for p in files:
self.assertLessEqual(p.stat().st_size, 220)
self.assertEqual(p.stat().st_mode & 0o777, 0o600)
self.assertNotIn("SYNTHETIC_SECRET", p.read_text())
def test_no_symlink_hardlink_or_permissive_log_is_written(self):
for kind in ["symlink", "hardlink", "permissions"]:
with self.subTest(kind=kind), tempfile.TemporaryDirectory() as td:
log = monitor.PrivateLog(td, "fixture")
p = log.path
target = Path(td) / "untouched"
target.write_text("unchanged")
target.chmod(0o600)
if kind == "symlink":
p.symlink_to(target)
elif kind == "hardlink":
os.link(target, p)
else:
p.write_text("unchanged")
p.chmod(0o644)
with self.assertRaises((OSError, monitor.ConnectionMonitorError)):
log.write("must not be stored")
self.assertEqual(target.read_text(), "unchanged")
log.close()
def test_fifo_paths_are_rejected_without_waiting_for_a_writer(self):
with tempfile.TemporaryDirectory() as td:
log = monitor.PrivateLog(td, "fixture")
os.mkfifo(log.path, 0o600)
with self.assertRaises((OSError, monitor.ConnectionMonitorError)):
log.write("must not block")
log.close()
endpoint = Path(td) / "health.url"
os.mkfifo(endpoint, 0o600)
with self.assertRaises((OSError, monitor.ConnectionMonitorError)):
monitor.health_endpoint(endpoint)
def test_capture_redacts_secret_split_across_reads_and_drops_oversize_line(self):
with tempfile.TemporaryDirectory() as td:
secret = "FAKE_CROSS_CHUNK_CREDENTIAL"
log = monitor.PrivateLog(td, secret)
read_fd, write_fd = os.pipe()
errors, stop = [], threading.Event()
stream = os.fdopen(read_fd, "rb", buffering=0)
worker = threading.Thread(target=monitor.capture_output, args=(stream, log, errors, stop))
worker.start()
for part in [b"debug " + secret[:9].encode(), secret[9:].encode() + b"\n", b"A" * (monitor.MAX_LINE + 100), b"\n"]:
os.write(write_fd, part)
os.close(write_fd)
worker.join(timeout=4)
self.assertFalse(worker.is_alive())
self.assertFalse(errors)
data = log.path.read_text()
self.assertNotIn(secret, data)
self.assertIn("debug [REDACTED]", data)
self.assertIn("Oversized client output line omitted", data)
stream.close()
log.close()
def test_secret_headers_and_quoted_values_are_not_partially_persisted(self):
for text in ['Cookie: first=abc; second=def', 'Authorization: Bearer abc xyz',
'\"password\": \"secret with spaces\"', 'url?access_token=abc&next=def']:
clean = monitor.redact(text, "fixture")
for fragment in ["abc", "def", "secret with spaces", "first=", "second="]:
self.assertNotIn(fragment, clean)
self.assertIn("[REDACTED]", clean)
def test_normalization_cannot_reconstruct_a_secret_after_redaction(self):
secret = "FAKE_CREDENTIAL"
for middle in ["\x1b[31m", "\x01", "\u200b"]:
value = "FAKE_" + middle + "CREDENTIAL"
clean = monitor.redact(value, secret)
self.assertNotIn(secret, clean)
self.assertNotIn("CREDENTIAL", clean)
self.assertIn("[REDACTED]", clean)
def test_proxy_url_userinfo_is_redacted(self):
for value in ["proxy http://user:FAKE_PASSWORD@proxy.example:80/path", "https://name:p%40ss@host.example/next"]:
clean = monitor.redact(value, "different_runtime_fixture")
for fragment in ["user:", "FAKE_PASSWORD", "name:", "p%40ss"]:
self.assertNotIn(fragment, clean)
self.assertIn("[REDACTED]@", clean)
def test_non_tty_progress_does_not_animate_or_repeat_same_state(self):
out = io.StringIO()
progress = monitor.Progress(out)
for _ in range(8):
progress.change("checking", "Checking connection")
progress.tick()
self.assertEqual(out.getvalue(), "Checking connection\n")
class ForegroundFailureTests(unittest.TestCase):
def command(self, code):
return [sys.executable, "-u", "-c", code,
"--control-plane.tunnel-id", ID, "--mcp.server-url", "url=" + MCP]
def test_early_failure_has_clear_error_and_no_technical_terminal_dump(self):
with tempfile.TemporaryDirectory() as td:
out = io.StringIO()
result = tunnel.run_foreground(self.command("import sys;print('TECHNICAL_PRIVATE_DETAIL');sys.exit(3)"),
"SYNTHETIC_KEY", state=td, output=out)
self.assertEqual(result, 3)
self.assertIn("[ERROR]", out.getvalue())
self.assertNotIn("[OK]", out.getvalue())
self.assertNotIn("TECHNICAL_PRIVATE_DETAIL", out.getvalue())
self.assertIn("TECHNICAL_PRIVATE_DETAIL", (Path(td) / "logs/chatgpt/connection.log").read_text())
def test_exit_before_verified_state_does_not_report_success(self):
seen = {}
def spawn(*args, **kwargs):
seen["child"] = subprocess.Popen(*args, **kwargs)
return seen["child"]
def probe(*args):
seen["child"].wait(timeout=3)
return "ready", "synthetic readiness arrived after exit"
with tempfile.TemporaryDirectory() as td:
out = io.StringIO()
with mock.patch.object(monitor, "probe", side_effect=probe):
result = tunnel.run_foreground(self.command("pass"), "SYNTHETIC_KEY", state=td, output=out, popen=spawn)
self.assertEqual(result, 1)
self.assertNotIn("[OK]", out.getvalue())
def test_cleanup_failure_does_not_skip_signal_restoration(self):
import signal
before = {sig: signal.getsignal(sig) for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP)}
with tempfile.TemporaryDirectory() as td:
out = io.StringIO()
with mock.patch.object(tunnel, "_stop_owned", side_effect=OSError("synthetic cleanup failure")):
rc = tunnel.run_foreground(self.command("pass"), "SYNTHETIC_KEY", state=td, output=out)
self.assertEqual(rc, 1)
self.assertIn("cleanup could not be fully verified", out.getvalue())
self.assertEqual({sig: signal.getsignal(sig) for sig in before}, before)
def test_verified_connection_loss_is_explicit_and_ctrl_c_still_cleans_up(self):
states = iter([("ready", "synthetic verified"), ("access", "synthetic poll lost")])
def probe(*args):
try:
return next(states)
except StopIteration:
raise KeyboardInterrupt
with tempfile.TemporaryDirectory() as td:
out = io.StringIO()
with mock.patch.object(monitor, "probe", side_effect=probe):
result = tunnel.run_foreground(self.command("import time;time.sleep(60)"), "SYNTHETIC_KEY", state=td, output=out)
self.assertEqual(result, 130)
self.assertIn("[RETRYING]", out.getvalue())
self.assertIn("[STOPPED]", out.getvalue())
self.assertNotIn("Traceback", out.getvalue())
+104
View File
@@ -0,0 +1,104 @@
import os
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import console
class ConsoleTests(unittest.TestCase):
def test_remote_alias_cannot_create_a_local_console(self):
with tempfile.TemporaryDirectory() as path, mock.patch.object(Path, "mkdir") as mkdir:
with self.assertRaisesRegex(RuntimeError, "REMOTE_CONSOLE_SSH_CONFIGURATION_REQUIRED"):
console.context(Path(path) / "profile", "a-different-node", create=True)
mkdir.assert_not_called()
def test_profile_socket_and_job_roots_are_isolated(self):
with tempfile.TemporaryDirectory() as path:
one = console.context(Path(path) / "one")
two = console.context(Path(path) / "two")
self.assertNotEqual(one["socket"], two["socket"])
self.assertNotEqual(one["session"], two["session"])
self.assertEqual(one["jobs_root"], Path(path) / "one/home/.local/share/framebridge/fbterm")
self.assertLess(len(str(one["socket"]).encode()), 108)
def desktop(self, *, display=":0", vt="7", user=None):
return SimpleNamespace(returncode=0, stdout=(
"Type=x11\nUser=" + str(os.getuid() if user is None else user)
+ "\nActive=yes\nRemote=no\nDisplay=" + display + "\nVTNr=" + vt + "\nTTY=\nLeader=3668\n"))
def test_lightdm_empty_tty_does_not_hide_active_user_desktop(self):
with mock.patch.object(console.subprocess, "run", return_value=self.desktop()), \
mock.patch.object(Path, "read_text", return_value="tty7\n"):
self.assertTrue(console._active_graphical_display({"DISPLAY": ":0", "XDG_SESSION_ID": "19"}))
def test_private_virtual_display_is_not_chosen_as_user_desktop(self):
with mock.patch.object(console.subprocess, "run", return_value=self.desktop()), \
mock.patch.object(Path, "read_text", return_value="tty7\n"):
self.assertFalse(console._active_graphical_display({"DISPLAY": ":99", "XDG_SESSION_ID": "19"}))
def test_changed_foreground_vt_prevents_gui_auto_open(self):
with mock.patch.object(console.subprocess, "run", return_value=self.desktop()), \
mock.patch.object(Path, "read_text", return_value="tty1\n"):
self.assertFalse(console._active_graphical_display({"DISPLAY": ":0", "XDG_SESSION_ID": "19"}))
def test_foreign_user_session_is_not_selected(self):
with mock.patch.object(console.subprocess, "run", return_value=self.desktop(user=os.getuid() + 1)), \
mock.patch.object(Path, "read_text", return_value="tty7\n"):
self.assertFalse(console._active_graphical_display({"DISPLAY": ":0", "XDG_SESSION_ID": "19"}))
def test_noninteractive_terminal_fails_before_starting_a_console(self):
with tempfile.TemporaryDirectory() as path:
ctx = console.context(Path(path) / "one")
with mock.patch.object(console, "context", return_value=ctx), \
mock.patch.object(console, "_active_graphical_display", return_value=False), \
mock.patch.object(console.os, "isatty", return_value=False), \
mock.patch.object(console.subprocess, "run") as run:
with self.assertRaisesRegex(RuntimeError, "CONSOLE_INTERACTIVE_TERMINAL_REQUIRED"):
console.run(ctx["state"])
run.assert_not_called()
def test_terminal_attach_keeps_shared_identity_and_profile_jobs(self):
with tempfile.TemporaryDirectory() as path:
ctx = console.context(Path(path) / "one")
with mock.patch.object(console, "context", return_value=ctx), \
mock.patch.object(console, "_active_graphical_display", return_value=False), \
mock.patch.object(console.os, "isatty", return_value=True), \
mock.patch.object(console.subprocess, "run", return_value=SimpleNamespace(returncode=0)), \
mock.patch.object(console.os, "execve") as execute:
self.assertEqual(console.run(ctx["state"], inside=True), 0)
program, args, env = execute.call_args.args
self.assertEqual(program, "/usr/bin/tmux")
self.assertIn(str(ctx["socket"]), args)
self.assertIn("=" + ctx["session"], args)
self.assertEqual(env["FRAMEBRIDGE_FBTERM_ROOT"], str(ctx["jobs_root"]))
self.assertNotIn("sudo", args)
def test_layout_failure_does_not_attach_or_replace_a_shell(self):
with tempfile.TemporaryDirectory() as path:
ctx = console.context(Path(path) / "one")
with mock.patch.object(console, "context", return_value=ctx), \
mock.patch.object(console, "_active_graphical_display", return_value=False), \
mock.patch.object(console.os, "isatty", return_value=True), \
mock.patch.object(console.subprocess, "run", return_value=SimpleNamespace(returncode=1)), \
mock.patch.object(console.os, "execve") as execute:
with self.assertRaisesRegex(RuntimeError, "CONSOLE_LAYOUT_FAILED"):
console.run(ctx["state"], inside=True)
execute.assert_not_called()
def test_dangling_socket_symlink_is_refused(self):
with tempfile.TemporaryDirectory() as path:
sock = Path(path) / "console.sock"
sock.symlink_to(Path(path) / "missing")
with self.assertRaisesRegex(RuntimeError, "CONSOLE_SOCKET_UNTRUSTED"):
console._socket_exists(sock)
def test_public_runtime_directory_is_refused(self):
with tempfile.TemporaryDirectory() as path:
os.chmod(path, 0o755)
with self.assertRaisesRegex(RuntimeError, "CONSOLE_RUNTIME_DIRECTORY_UNTRUSTED"):
console._private_runtime(Path(path))
if __name__ == "__main__":
unittest.main()
+122
View File
@@ -0,0 +1,122 @@
import os
from pathlib import Path
import subprocess
import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import console
class ConsoleNestingTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
root = Path(self.directory.name)
self.ctx = {
"state": root / "profile", "node": "test-node", "runtime": root,
"socket": root / "console.sock", "session": "FB-test-profile",
"jobs_root": root / "profile/home/.local/share/framebridge/fbterm",
}
def environment(self, *, socket=None, pane="%0"):
return {"TMUX": str(socket or self.ctx["socket"]) + ",1234,0",
"TMUX_PANE": pane}
def invoke(self, env, query=None, *, graphical=False):
def respond(args, **kwargs):
if "list-panes" in args:
if isinstance(query, Exception):
raise query
return query or SimpleNamespace(returncode=0, stdout="%0\n%1\n")
return SimpleNamespace(returncode=0, stdout="")
with mock.patch.dict(os.environ, env, clear=True), \
mock.patch.object(console, "context", return_value=self.ctx), \
mock.patch.object(console, "_active_graphical_display", return_value=graphical), \
mock.patch.object(console.os, "isatty", return_value=True), \
mock.patch.object(console.shutil, "which", return_value="/usr/bin/gnome-terminal"), \
mock.patch.object(console.subprocess, "run", side_effect=respond) as run, \
mock.patch.object(console.os, "execve") as execute:
try:
result = console.run(self.ctx["state"], mode="gui" if graphical else "tty")
except RuntimeError as error:
result = error
return result, run, execute
def assert_blocked_before_layout(self, result, run, execute, error):
self.assertIsInstance(result, RuntimeError)
self.assertIn(error, str(result))
execute.assert_not_called()
for call in run.call_args_list:
self.assertIn("list-panes", call.args[0])
self.assertNotIn("--apply", call.args[0])
def test_same_console_pane_is_rejected_before_layout_and_attach(self):
result, run, execute = self.invoke(self.environment())
self.assert_blocked_before_layout(result, run, execute, "CONSOLE_ALREADY_ATTACHED")
run.assert_called_once()
args = run.call_args.args[0]
self.assertIn("=" + self.ctx["session"], args)
self.assertEqual(args[-2:], ["-F", "#{pane_id}"])
def test_socket_path_alias_does_not_bypass_self_attach_check(self):
alias = str(self.ctx["socket"].parent) + "/./console.sock"
env = {"TMUX": alias + ",1234,0", "TMUX_PANE": "%0"}
result, run, execute = self.invoke(env)
self.assert_blocked_before_layout(result, run, execute, "CONSOLE_ALREADY_ATTACHED")
def test_unrelated_profile_can_attach_normally(self):
result, run, execute = self.invoke(self.environment(socket=self.ctx["socket"].with_name("other.sock")))
self.assertEqual(result, 0)
execute.assert_called_once()
run.assert_called_once()
self.assertIn("--apply", run.call_args.args[0])
def test_other_session_without_shared_panes_can_attach(self):
result, run, execute = self.invoke(self.environment(pane="%9"))
self.assertEqual(result, 0)
execute.assert_called_once()
self.assertEqual(run.call_count, 2)
self.assertIn("--apply", run.call_args.args[0])
def test_plain_terminal_can_attach_normally(self):
result, run, execute = self.invoke({})
self.assertEqual(result, 0)
execute.assert_called_once()
run.assert_called_once()
self.assertIn("--apply", run.call_args.args[0])
def test_failed_membership_query_does_not_assume_attach_is_safe(self):
result, run, execute = self.invoke(
self.environment(), SimpleNamespace(returncode=1, stdout=""))
self.assert_blocked_before_layout(result, run, execute, "CONSOLE_TMUX_CONTEXT_UNVERIFIED")
def test_query_timeout_does_not_attach(self):
result, run, execute = self.invoke(
self.environment(), subprocess.TimeoutExpired(["tmux"], 5))
self.assert_blocked_before_layout(result, run, execute, "CONSOLE_TMUX_CONTEXT_UNVERIFIED")
def test_invalid_pane_metadata_does_not_attach(self):
result, run, execute = self.invoke(
self.environment(), SimpleNamespace(returncode=0, stdout="not-a-pane\n"))
self.assert_blocked_before_layout(result, run, execute, "CONSOLE_TMUX_CONTEXT_UNVERIFIED")
def test_malformed_matching_context_does_not_run_tmux(self):
result, run, execute = self.invoke(self.environment(pane="invalid"))
self.assert_blocked_before_layout(result, run, execute, "CONSOLE_TMUX_CONTEXT_UNVERIFIED")
run.assert_not_called()
def test_independent_gui_window_keeps_clean_environment(self):
result, run, execute = self.invoke(self.environment(), graphical=True)
self.assertEqual(result, 0)
execute.assert_not_called()
run.assert_called_once()
args, kwargs = run.call_args
self.assertEqual(args[0][0], "/usr/bin/gnome-terminal")
self.assertNotIn("TMUX", kwargs["env"])
self.assertNotIn("TMUX_PANE", kwargs["env"])
if __name__ == "__main__":
unittest.main()
+224
View File
@@ -0,0 +1,224 @@
"""Opt-in startup tests use only owned temporary homes and harmless stubs."""
import json
import os
from pathlib import Path
import shlex
import stat
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "expanded"))
from framebridge_expanded import console_startup as startup
@unittest.skipIf(os.geteuid() == 0, "Unprivileged user profile tests")
class ConsoleStartupTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="fb-login-startup-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.home = self.root / "home"
self.home.mkdir(mode=0o700)
self.state = self.root / "state"
def existing(self, name=".profile", data=b"# user's existing startup\n", mode=0o640):
path = self.home / name
path.write_bytes(data)
path.chmod(mode)
return path
def assert_ok(self, value):
self.assertTrue(value.get("ok"), value)
return value
def manifest(self):
return json.loads((self.state / "console-login/manifest.json").read_text())
def test_existing_bytes_mode_and_exact_private_backup_restore(self):
data = b"printf '%s' 'existing login content'"
path = self.existing(".bash_profile", data)
result = self.assert_ok(startup.enable(self.state, home=self.home))
self.assertTrue(result["enabled"])
self.assertEqual(result["startup_file"], str(path))
self.assertTrue(path.read_bytes().startswith(data + b"\n"))
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o640)
record = self.manifest()
backup = self.state / "console-login" / record["backup_name"]
self.assertEqual(backup.read_bytes(), data)
self.assertEqual(stat.S_IMODE(backup.stat().st_mode), 0o600)
self.assertTrue(self.assert_ok(startup.status(self.state, home=self.home))["enabled"])
result = self.assert_ok(startup.disable(self.state, home=self.home))
self.assertFalse(result["enabled"])
self.assertEqual(path.read_bytes(), data)
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o640)
def test_effective_bash_startup_priority(self):
self.existing(".profile", b"# lower priority\n")
login = self.existing(".bash_login", b"# selected login\n")
result = self.assert_ok(startup.enable(self.state, home=self.home))
self.assertEqual(result["startup_file"], str(login))
self.assertEqual((self.home / ".profile").read_bytes(), b"# lower priority\n")
def test_new_profile_is_removed_on_disable(self):
self.assertTrue(self.assert_ok(startup.enable(self.state, home=self.home))["changed"])
path = self.home / ".profile"
self.assertTrue(path.is_file())
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600)
self.assert_ok(startup.disable(self.state, home=self.home))
self.assertFalse(path.exists())
def test_enable_and_disable_are_idempotent(self):
path = self.existing()
self.assert_ok(startup.enable(self.state, home=self.home))
installed = path.read_bytes()
again = self.assert_ok(startup.enable(self.state, home=self.home))
self.assertFalse(again["changed"])
self.assertEqual(path.read_bytes(), installed)
self.assertEqual(len(list((self.state / "console-login").glob("backup-*.bin"))), 1)
self.assert_ok(startup.disable(self.state, home=self.home))
self.assertFalse(self.assert_ok(startup.disable(self.state, home=self.home))["changed"])
def test_reenable_preserves_previous_recovery_history(self):
path = self.existing(data=b"# baseline one\n")
self.assert_ok(startup.enable(self.state, home=self.home))
self.assert_ok(startup.disable(self.state, home=self.home))
path.write_bytes(b"# baseline two\n")
self.assert_ok(startup.enable(self.state, home=self.home))
self.assertEqual(len(list((self.state / "console-login").glob("backup-*.bin"))), 2)
self.assertEqual(len(list((self.state / "console-login").glob("history-*.json"))), 1)
self.assert_ok(startup.disable(self.state, home=self.home))
self.assertEqual(path.read_bytes(), b"# baseline two\n")
def test_later_edits_are_never_overwritten(self):
path = self.existing()
self.assert_ok(startup.enable(self.state, home=self.home))
path.write_bytes(path.read_bytes() + b"# later user change\n")
changed = path.read_bytes()
for operation in (startup.disable, startup.enable, startup.status):
reply = operation(self.state, home=self.home)
self.assertFalse(reply["ok"])
self.assertEqual(reply["error"], "CONSOLE_LOGIN_STARTUP_CHANGED")
self.assertEqual(path.read_bytes(), changed)
def test_symlink_and_nonregular_startup_are_refused(self):
target = self.root / "outside"
target.write_bytes(b"# unchanged outside\n")
(self.home / ".bash_profile").symlink_to(target)
reply = startup.enable(self.state, home=self.home)
self.assertFalse(reply["ok"])
self.assertEqual(reply["error"], "CONSOLE_LOGIN_SYMLINK_REFUSED")
self.assertEqual(target.read_bytes(), b"# unchanged outside\n")
(self.home / ".bash_profile").unlink()
(self.home / ".bash_profile").mkdir()
self.assertEqual(startup.enable(self.state, home=self.home)["error"],
"CONSOLE_LOGIN_FILE_NOT_REGULAR")
def test_competing_profile_is_refused(self):
path = self.existing()
self.assert_ok(startup.enable(self.state, home=self.home))
installed = path.read_bytes()
other = self.root / "other-state"
reply = startup.enable(other, home=self.home)
self.assertEqual(reply["error"], "CONSOLE_LOGIN_OTHER_PROFILE_ENABLED")
self.assertEqual(path.read_bytes(), installed)
self.assertFalse((other / "console-login/manifest.json").exists())
def test_foreign_untracked_block_is_refused(self):
path = self.existing(data=b"# BEGIN FRAMEBRIDGE CONSOLE LOGIN v1 foreign\n")
before = path.read_bytes()
reply = startup.enable(self.state, home=self.home)
self.assertEqual(reply["error"], "CONSOLE_LOGIN_OTHER_PROFILE_ENABLED")
self.assertEqual(path.read_bytes(), before)
def test_priority_change_is_reported_but_own_hook_can_be_removed(self):
path = self.existing(data=b"# restored profile\n")
self.assert_ok(startup.enable(self.state, home=self.home))
higher = self.existing(".bash_profile", b"# newly preferred file\n")
reply = startup.status(self.state, home=self.home)
self.assertEqual(reply["error"], "CONSOLE_LOGIN_STARTUP_PRIORITY_CHANGED")
self.assert_ok(startup.disable(self.state, home=self.home))
self.assertEqual(path.read_bytes(), b"# restored profile\n")
self.assertEqual(higher.read_bytes(), b"# newly preferred file\n")
def test_modified_backup_refuses_restore(self):
path = self.existing()
self.assert_ok(startup.enable(self.state, home=self.home))
installed = path.read_bytes()
record = self.manifest()
backup = self.state / "console-login" / record["backup_name"]
backup.write_bytes(b"# tampered recovery\n")
reply = startup.disable(self.state, home=self.home)
self.assertEqual(reply["error"], "CONSOLE_LOGIN_BACKUP_MISMATCH")
self.assertEqual(path.read_bytes(), installed)
def test_state_symlink_is_refused(self):
real = self.root / "real-state"
real.mkdir(mode=0o700)
self.state.symlink_to(real, target_is_directory=True)
reply = startup.enable(self.state, home=self.home)
self.assertEqual(reply["error"], "CONSOLE_LOGIN_SYMLINK_REFUSED")
self.assertFalse((real / "console-login").exists())
def test_status_has_no_write_side_effects(self):
reply = self.assert_ok(startup.status(self.state, home=self.home))
self.assertFalse(reply["enabled"])
self.assertFalse(self.state.exists())
self.assertFalse((self.home / ".framebridge-console-login.lock").exists())
def test_default_home_uses_passwd_not_profile_environment_home(self):
with mock.patch.object(startup.pwd, "getpwuid",
return_value=type("Identity", (), {"pw_dir": str(self.home)})()):
with mock.patch.dict(os.environ, {"HOME": str(self.root / "wrong-home")}):
reply = self.assert_ok(startup.enable(self.state))
self.assertEqual(reply["startup_file"], str(self.home / ".profile"))
def test_login_guards_and_quoted_state_are_executed_in_fixture_only(self):
fixture_bin = self.root / "bin"
fixture_bin.mkdir()
tty = fixture_bin / "tty"
tty.write_text("#!/bin/sh\nprintf '%s\\n' \"$" + "{FB_TEST_TTY-/dev/tty1}\"\n")
tty.chmod(0o700)
marker = self.root / "invocation.json"
stub = fixture_bin / "fb-stub"
stub.write_text("#!/usr/bin/python3\nimport json,sys\nfrom pathlib import Path\n"
+ "Path(" + repr(str(marker)) + ").write_text(json.dumps(sys.argv[1:]))\n")
stub.chmod(0o700)
weird = self.root / "state with ' quote $(touch SHOULD_NOT_EXIST)"
with mock.patch.object(startup, "FB_EXECUTABLE", str(stub)):
self.assert_ok(startup.enable(weird, home=self.home))
profile = self.home / ".profile"
subprocess.run(["/bin/sh", "-n", str(profile)], check=True)
base_env = dict(os.environ, PATH=str(fixture_bin) + ":" + os.environ["PATH"],
SSH_TTY="", TMUX="", FB_TEST_TTY="/dev/tty1")
command = ["/bin/bash", "--noprofile", "--norc", "--login", "-c",
". " + shlex.quote(str(profile))]
for override in ({"SSH_TTY": "/dev/pts/9"}, {"TMUX": "test-client"},
{"FB_TEST_TTY": "/dev/tty2"}):
subprocess.run(command, env=dict(base_env, **override), check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
self.assertFalse(marker.exists())
nonlogin = ["/bin/bash", "--noprofile", "--norc", "-c",
". " + shlex.quote(str(profile))]
subprocess.run(nonlogin, env=base_env, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
self.assertFalse(marker.exists())
subprocess.run(command, env=base_env, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
self.assertEqual(json.loads(marker.read_text()),
["--state-dir", str(weird), "console", "--mode", "tty"])
self.assertFalse((self.root / "SHOULD_NOT_EXIST").exists())
class UserOnlyTests(unittest.TestCase):
def test_root_context_is_rejected_before_any_path_change(self):
with mock.patch.object(startup.os, "geteuid", return_value=0):
reply = startup.enable("/does/not/exist", home="/also/not/used")
self.assertEqual(reply["error"], "CONSOLE_LOGIN_USER_REQUIRED")
if __name__ == "__main__":
unittest.main()
+105
View File
@@ -0,0 +1,105 @@
"""Source-availability diagnostics for the read-only FBTERM catalogue."""
import errno
import json
from pathlib import Path
import tempfile
import unittest
from unittest import mock
from framebridge_expanded.console_ui import fbterm_dashboard as dashboard
class DashboardSourcesTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
def legacy_job(self):
job = self.root / "jobs" / "legacy-job"
job.mkdir(parents=True)
(job / "exit.code").write_text("0\n")
return job
def scan(self):
catalogue = dashboard.Catalogue(self.root)
catalogue.scan()
return catalogue
def test_missing_optional_receipts_keeps_legacy_exit_without_read_errors(self):
self.legacy_job()
catalogue = self.scan()
self.assertEqual(catalogue.errors, 0)
self.assertEqual(catalogue.total, 1)
self.assertEqual(catalogue.counts, {"EXIT": 1})
self.assertEqual(catalogue.recent[0].exit_code, 0)
self.assertFalse((self.root / "receipts").exists())
def test_receipts_permission_denied_remains_a_source_error(self):
self.legacy_job()
receipts = self.root / "receipts"
receipts.mkdir()
real_scandir = dashboard.os.scandir
def scandir(path):
if Path(path) == receipts:
raise PermissionError(errno.EACCES, "fixture denied", str(path))
return real_scandir(path)
with mock.patch.object(dashboard.os, "scandir", side_effect=scandir):
catalogue = self.scan()
self.assertEqual(catalogue.errors, 1)
self.assertEqual(catalogue.counts, {"EXIT": 1})
def test_receipts_that_is_not_a_directory_remains_a_source_error(self):
self.legacy_job()
(self.root / "receipts").write_text("not a directory")
catalogue = self.scan()
self.assertEqual(catalogue.errors, 1)
self.assertEqual(catalogue.counts, {"EXIT": 1})
def test_missing_required_jobs_remains_a_source_error(self):
catalogue = self.scan()
self.assertEqual(catalogue.errors, 1)
self.assertEqual(catalogue.total, 0)
self.assertFalse((self.root / "jobs").exists())
self.assertFalse((self.root / "receipts").exists())
def test_receipt_entry_disappearing_during_iteration_remains_a_read_error(self):
self.legacy_job()
receipts = self.root / "receipts"
receipts.mkdir()
entry = mock.Mock()
entry.name = "vanished.json"
entry.is_file.side_effect = FileNotFoundError(
errno.ENOENT, "fixture entry disappeared")
iterator = mock.MagicMock()
iterator.__enter__.return_value = iter([entry])
real_scandir = dashboard.os.scandir
def scandir(path):
return iterator if Path(path) == receipts else real_scandir(path)
with mock.patch.object(dashboard.os, "scandir", side_effect=scandir):
catalogue = self.scan()
self.assertEqual(catalogue.errors, 1)
self.assertEqual(catalogue.counts, {"EXIT": 1})
def test_completed_receipt_without_job_is_mismatch_not_a_source_error(self):
(self.root / "jobs").mkdir()
receipts = self.root / "receipts"
receipts.mkdir()
receipt = {
"schema": 4, "job_id": "completed-tombstone", "phase": "COMPLETED",
"started_at": 1, "finished_at": 2, "exit_code": 0,
}
(receipts / "completed-tombstone.json").write_text(json.dumps(receipt))
catalogue = self.scan()
self.assertEqual(catalogue.errors, 0)
self.assertEqual(catalogue.total, 1)
self.assertEqual(catalogue.counts, {"MISMATCH": 1})
self.assertIsNone(catalogue.recent[0].exit_code)
if __name__ == "__main__":
unittest.main()
+134
View File
@@ -0,0 +1,134 @@
import json
import unittest
from unittest.mock import patch
from framebridge_expanded import display_context as dc
class DisplayBindingTests(unittest.TestCase):
def setUp(self):
tools = patch.object(dc, "missing_graphical_commands", return_value=[])
tools.start()
self.addCleanup(tools.stop)
def context(self, **changes):
value = dict(kind="physical-x11", physical=True, session_id="c1", uid=1000,
leader=123, active_vt="tty7", seat="seat0", vtnr=7,
display=":0", authority="/private/user/.Xauthority")
value.update(changes)
return value
def bound(self, **changes):
context = self.context(**changes)
context["profile"] = "/profile/a"
return {"DISPLAY": context["display"], dc.BINDING: json.dumps(context)}
def test_auto_prefers_verified_physical_over_inherited_display(self):
env = {"DISPLAY": ":199", "XAUTHORITY": "/other/authority"}
with patch.object(dc, "_physical", return_value=self.context()):
dc.configure(env, "auto", "/profile/a")
self.assertEqual(env["DISPLAY"], ":0")
self.assertEqual(env["XDG_SESSION_TYPE"], "x11")
self.assertEqual(env["GDK_BACKEND"], "x11")
self.assertEqual(env["XAUTHORITY"], "/private/user/.Xauthority")
self.assertTrue(dc.describe(env)["physical"])
def test_explicit_virtual_does_not_resolve_physical(self):
env = {}
with patch.object(dc, "_physical") as physical:
dc.configure(env, "yes", "/profile/a")
physical.assert_not_called()
dc.bind_virtual(env, "/profile/a", explicit=True)
self.assertFalse(dc.describe(env)["physical"])
self.assertEqual(dc.describe(env)["kind"], "virtual-x11")
def test_auto_without_physical_session_is_text_not_virtual(self):
env = {"DISPLAY": ":199"}
with patch.object(dc, "_physical", side_effect=dc.DisplayContextError("NO_SESSION")):
dc.configure(env, "auto", "/profile/a")
self.assertEqual(dc.describe(env)["kind"], "text")
self.assertFalse(dc.describe(env)["physical"])
self.assertFalse(dc.describe(env)["explicit"])
def test_no_headless_requires_verified_session(self):
env = {"DISPLAY": ":199"}
with patch.object(dc, "_physical", side_effect=dc.DisplayContextError("NO_SESSION")):
dc.configure(env, "no", "/profile/a")
self.assertEqual(dc.describe(env)["kind"], "unavailable")
with self.assertRaisesRegex(dc.DisplayContextError, "CUA_DISPLAY_UNAVAILABLE"):
dc.check(env)
def test_changed_vt_session_or_user_is_rejected(self):
for changed in (dict(active_vt="tty1", vtnr=1),
dict(session_id="c2"), dict(uid=1001), dict(leader=124)):
with self.subTest(changed=changed):
with patch.object(dc, "_physical", return_value=self.context(**changed)):
with self.assertRaisesRegex(dc.DisplayContextError, "CUA_FOREGROUND_SESSION_CHANGED"):
dc.check(self.bound())
def test_input_display_must_match_physical_binding(self):
env = self.bound()
env["DISPLAY"] = ":199"
with patch.object(dc, "_physical", return_value=self.context()):
with self.assertRaisesRegex(dc.DisplayContextError, "CUA_DISPLAY_BINDING_MISMATCH"):
dc.check(env)
def test_virtual_display_cannot_silently_switch(self):
env = {"DISPLAY": ":140"}
dc.bind_virtual(env, "/profile/a", explicit=True)
dc.check(env)
env["DISPLAY"] = ":0"
with self.assertRaisesRegex(dc.DisplayContextError, "VIRTUAL_DISPLAY_BINDING_MISMATCH"):
dc.check(env)
def test_public_metadata_omits_authority_reference(self):
context = dc.describe(self.bound())
self.assertNotIn("authority", context)
self.assertTrue(context["physical"])
def test_other_profile_binding_is_not_adopted(self):
env = self.bound()
with patch.object(dc, "_physical", return_value=self.context(session_id="c2")):
dc.configure(env, "auto", "/profile/b")
self.assertEqual(dc.describe(env)["session_id"], "c2")
self.assertEqual(dc.describe(env)["profile"], "/profile/b")
def test_invalid_binding_is_rejected(self):
with self.assertRaisesRegex(dc.DisplayContextError, "DISPLAY_BINDING_INVALID"):
dc.check({dc.BINDING: "invalid"})
if __name__ == "__main__":
unittest.main()
class DisplayBindingAuditTests(unittest.TestCase):
def test_headless_no_late_failure_refuses_virtual_fallback(self):
env = {dc.BINDING: json.dumps({"kind": "physical-x11", "physical": True})}
with self.assertRaisesRegex(dc.DisplayContextError, "NO_VIRTUAL_FALLBACK"):
dc.private_display_required(env, "no", False)
self.assertFalse(dc.private_display_required(env, "no", True))
def test_auto_late_failure_does_not_start_virtual_desktop(self):
env = {dc.BINDING: json.dumps({"kind": "physical-x11", "physical": True})}
self.assertFalse(dc.private_display_required(env, "auto", False))
def test_headless_no_ignores_inherited_virtual_binding(self):
env = {dc.BINDING: json.dumps({"kind": "virtual-x11", "profile": "/fixture"}),
"DISPLAY": ":199"}
with patch.object(dc, "_physical", side_effect=dc.DisplayContextError("NO_SESSION")):
dc.configure(env, "no", "/fixture")
self.assertEqual(dc.describe(env)["kind"], "unavailable")
def test_explicit_virtual_ignores_inherited_physical_binding(self):
env = {dc.BINDING: json.dumps({"kind": "physical-x11", "profile": "/fixture"})}
with patch.object(dc, "_physical") as physical:
dc.configure(env, "yes", "/fixture")
physical.assert_not_called()
self.assertEqual(dc.describe(env)["kind"], "virtual-x11")
self.assertTrue(dc.describe(env)["explicit"])
def test_non_object_binding_is_invalid_and_can_be_reconfigured(self):
env = {dc.BINDING: "[]"}
with self.assertRaisesRegex(dc.DisplayContextError, "DISPLAY_BINDING_INVALID"):
dc.check(env)
self.assertEqual(dc.describe(env)["kind"], "unavailable")
with patch.object(dc, "_physical", side_effect=dc.DisplayContextError("NO_SESSION")):
dc.configure(env, "auto", "/fixture")
self.assertEqual(dc.describe(env)["kind"], "text")
+144
View File
@@ -0,0 +1,144 @@
"""Real FBPAIR/1 identities and sockets in temporary, isolated profiles."""
import hashlib
import json
import os
import socket
import tempfile
import threading
import time
import unittest
import uuid
from pathlib import Path
from framebridge_expanded import broker, node
class BrokerNodeTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="framebridge-expanded-broker-test-")
self.root = Path(self.temp.name)
self.central = self.root / "central"
self.central.mkdir(mode=0o700)
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
self.port = sock.getsockname()[1]
self.url = "http://127.0.0.1:" + str(self.port)
(self.central / "config.json").write_text(json.dumps({
"schema": 1, "role": "central", "name": "central-test", "node_id": str(uuid.uuid4()),
"ports": {"broker": self.port}, "broker_url": self.url}))
self.server = broker.make_server(port=self.port, state_dir=self.central)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
broker.configure(state_dir=self.central)
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(2)
self.temp.cleanup()
def make_node(self, label):
state = self.root / label
state.mkdir(mode=0o700)
(state / "config.json").write_text(json.dumps({
"schema": 1, "role": "node", "name": label, "node_id": str(uuid.uuid4()),
"broker_url": self.url}))
return state
def pair(self, label):
state = self.make_node(label)
public = node.enroll(self.url, label, state)
pending = next(p for p in broker.list_pending() if p["node_id"] == public["node_id"])
response = broker.approve(pending["request_id"], public["node_id"])
self.assertTrue(response["ok"])
paired = node.poll_enrollment(state)
self.assertEqual(paired["state"], "paired")
self.assertFalse(node.status(state)["broker_confirm_required"])
return state, public["node_id"]
def test_http_remote_and_unauthenticated_control_are_rejected(self):
for value in ("http://10.0.0.53:1234", "http://example.com", "https://u:p@example.com", "https://example.com/#x"):
with self.assertRaises(broker.BrokerError):
broker.validate_endpoint(value)
with self.assertRaises(broker.BrokerError):
broker.post_json(self.url + "/fbpair/v1/control/nodes", {}, state_dir=self.central)
def test_approval_requires_exact_uuid_and_operator_auth(self):
state = self.make_node("node-a")
pending = node.enroll(self.url, "node-a", state)
item = broker.list_pending()[0]
self.assertEqual(item["node_id"], pending["node_id"])
self.assertTrue(item["fingerprint"])
self.assertEqual(item["fingerprint"], pending["fingerprint"])
with self.assertRaises(broker.BrokerError):
broker.approve(item["request_id"], str(uuid.uuid4()))
self.assertEqual(broker.list_confirmed_nodes(), [])
def test_two_nodes_identity_confirmation_heartbeat_and_real_read(self):
one, first = self.pair("node-a")
two, second = self.pair("node-b")
self.assertNotEqual(first, second)
before = broker.list_confirmed_nodes()
self.assertEqual(len(before), 2)
self.assertTrue(all(n["ready"] is False for n in before))
for state in (one, two):
self.assertTrue(node.run(state, once=True)["idle"])
self.assertTrue(all(n["ready"] is True for n in broker.list_confirmed_nodes()))
self.assertEqual(node.status(one)["node_id"], first)
stopped = threading.Event()
errors = []
def pump():
while not stopped.is_set():
try:
node.run(one, once=True)
except Exception as exc:
errors.append(type(exc).__name__)
return
stopped.wait(0.05)
worker = threading.Thread(target=pump, daemon=True)
worker.start()
try:
result = broker.request(first, "system.status", {}, timeout_s=5)
self.assertTrue(result["ok"], result)
self.assertIsInstance(result["result"], dict)
self.assertEqual(result["node_id"], first)
finally:
stopped.set()
worker.join(2)
self.assertFalse(errors)
def test_dispatch_rejects_wrong_node_and_bad_digest_before_adapter(self):
state, ident = self.pair("node-a")
called = []
script = "printf harmless"
request = {"request_id": "request-test", "node_id": str(uuid.uuid4()),
"expires_at": time.time() + 30, "operation": "admin.exec", "payload": {
"authorized": True, "job_id": "test-job", "timeout_s": 10,
"script": script, "sha256": hashlib.sha256(script.encode()).hexdigest()}}
self.assertEqual(node.dispatch_request(request, called.append, state)["error"], "REQUEST_NODE_MISMATCH")
request["node_id"] = ident
request["payload"]["sha256"] = "0" * 64
self.assertEqual(node.dispatch_request(request, called.append, state)["error"], "SCRIPT_SHA256_MISMATCH")
self.assertEqual(called, [])
def test_confirmed_identity_survives_broker_restart_but_readiness_does_not(self):
state, ident = self.pair("node-a")
node.run(state, once=True)
self.assertTrue(broker.list_confirmed_nodes()[0]["ready"])
self.server.shutdown()
self.server.server_close()
self.thread.join(2)
self.server = broker.make_server(port=self.port, state_dir=self.central)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
nodes = broker.list_confirmed_nodes()
self.assertEqual(nodes[0]["node_id"], ident)
self.assertFalse(nodes[0]["ready"])
node.run(state, once=True)
self.assertTrue(broker.list_confirmed_nodes()[0]["ready"])
if __name__ == "__main__":
unittest.main()
+139
View File
@@ -0,0 +1,139 @@
"""Routing contract tests; original read provider is exercised locally."""
import hashlib
from pathlib import Path
import socket
import sys
import types
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "expanded"))
from framebridge_expanded import fleet
LOCAL_ID = "c6d827a4-219f-48fc-82d8-f410176313ca"
REMOTE_ID = "6d88c5ae-343e-4413-a202-ccae2ae7694a"
class FleetRoutingTests(unittest.TestCase):
def setUp(self):
self.config = {"schema_version": 1, "role": "central", "name": "test-central", "node_id": LOCAL_ID}
self.local_config = mock.patch.object(fleet, "local_config", return_value=self.config)
self.local_config.start()
self.remote = {"alias": "test-node", "node_id": REMOTE_ID, "hostname": "test-node",
"transport": "broker_outbound", "target": "test-node",
"source": "confirmed_pairing", "capability_profile": "original-readonly",
"operational_transport_ready": True}
self.remote_nodes = mock.patch.object(fleet, "_remote_descriptors", return_value=[self.remote])
self.remote_nodes.start()
def tearDown(self):
self.remote_nodes.stop()
self.local_config.stop()
def test_original_local_read_provider(self):
value = fleet.node_read("LOCAL", "system.status", {})
self.assertTrue(value["ok"])
self.assertEqual(value["result"]["hostname"], socket.gethostname())
self.assertEqual(value["node"], "test-central")
self.assertEqual(value["transport"], "local")
def test_original_closed_read_schema(self):
value = fleet.node_read("LOCAL", "system.status", {"command": "uname"})
self.assertFalse(value["ok"])
self.assertEqual(value["error"], "FLEET_READ_ARGUMENTS_INVALID")
self.assertFalse(fleet.node_read("LOCAL", "exec", {})["ok"])
def test_discovery_identity_and_alias_routing(self):
value = fleet.discover_nodes()
self.assertTrue(value["ok"])
self.assertEqual({n["node_id"] for n in value["nodes"]}, {LOCAL_ID, REMOTE_ID})
self.assertEqual(fleet.resolve_node("test-node")["node_id"], REMOTE_ID)
self.assertEqual(fleet.resolve_node(REMOTE_ID)["alias"], "test-node")
self.assertEqual(fleet.resolve_node("LOCAL")["node_id"], LOCAL_ID)
def test_alias_collision_fails_closed(self):
self.remote["alias"] = "test-central"
value = fleet.discover_nodes()
self.assertFalse(value["ok"])
self.assertEqual(value["error"], "FLEET_IDENTITY_COLLISION")
def test_exec_guards_run_before_node_lookup(self):
with mock.patch.object(fleet, "resolve_node", side_effect=AssertionError("must not resolve")):
cases = [
({"authorized": False}, "AUTHORIZED_TRUE_REQUIRED"),
({"sha256": "0" * 64}, "SCRIPT_SHA256_MISMATCH"),
({"job_id": "../escape"}, "INVALID_JOB_ID"),
({"timeout_s": 0}, "INVALID_TIMEOUT"),
({"timeout_s": 601}, "INVALID_TIMEOUT"),
]
for extra, expected in cases:
kwargs = {"alias": "test-node", "script": "printf ok", "authorized": True}
kwargs.update(extra)
self.assertEqual(fleet.execute_node(**kwargs)["error"], expected)
def test_exec_remote_binding(self):
fake = types.ModuleType("framebridge_expanded.broker")
calls = []
def request(node_id, capability, args, timeout_s):
calls.append((node_id, capability, args, timeout_s))
return {"ok": True, "exit_code": "0", "job_id": args["job_id"],
"command_sha256": args["sha256"], "stdout": "safe"}
fake.request = request
with mock.patch("framebridge_expanded.broker", fake, create=True), mock.patch.dict(sys.modules, {"framebridge_expanded.broker": fake}):
from framebridge_expanded import broker
with mock.patch.object(broker, "request", side_effect=request):
value = fleet.execute_node("test-node", "printf ok", True, job_id="test-job", include_stdout=True)
self.assertTrue(value["ok"])
self.assertEqual(value["target"], "test-node")
self.assertEqual(value["mcp_script_sha256"], hashlib.sha256(b"printf ok").hexdigest())
self.assertEqual(calls[0][0:2], (REMOTE_ID, "admin.exec"))
def test_exec_rejects_mismatched_result(self):
fake = types.ModuleType("framebridge_expanded.broker")
fake.request = lambda *a, **k: {"ok": True, "job_id": "wrong", "command_sha256": "0"*64}
with mock.patch("framebridge_expanded.broker", fake, create=True), mock.patch.dict(sys.modules, {"framebridge_expanded.broker": fake}):
value = fleet.execute_node("test-node", "printf ok", True, job_id="bound-job")
self.assertFalse(value["ok"])
self.assertEqual(value["error"], "NODE_EXEC_JOB_BINDING_INVALID")
def test_cua_remote_original_wire_contract(self):
fake = types.ModuleType("framebridge_expanded.broker")
calls = []
def request(node_id, capability, args, timeout_s):
calls.append((node_id, capability, args, timeout_s))
return {"ok": True, "job_id": args["job_id"], "frame_output": ""}
fake.request = request
with mock.patch("framebridge_expanded.broker", fake, create=True), mock.patch.dict(sys.modules, {"framebridge_expanded.broker": fake}):
value = fleet.cua_node("test-node", {"action": "OBSERVE"}, True, job_id="cua-bound")
self.assertTrue(value["ok"])
self.assertEqual(calls[0][1], "admin.cua")
self.assertEqual(calls[0][2]["mode"], "EXEC")
self.assertEqual(calls[0][2]["request_type"], "cua")
self.assertEqual(calls[0][2]["job_id"], "cua-bound")
def test_local_alias_uses_original_adapter_without_network_envelope(self):
def adapter(payload):
return {"ok": True, "job_id": payload["job_id"],
"command_sha256": payload.get("sha256"), "stdout": "local"}
with mock.patch("framebridge_expanded.runtime.adapter_request", side_effect=adapter) as call:
result = fleet.execute_node("test-central", "printf ok", True, job_id="local-alias")
self.assertTrue(result["ok"])
self.assertEqual(result["target"], "LOCAL")
self.assertEqual(call.call_args.args[0]["mode"], "EXEC")
self.assertNotIn("request_id", call.call_args.args[0])
def test_cua_limits_preserved(self):
self.assertEqual(fleet.cua_node("test-node", {"action": "OBSERVE"}, True, 181)["error"],
"INVALID_CUA_TIMEOUT")
self.assertEqual(fleet.cua_node("test-node", {"action": "OBSERVE"}, False)["error"],
"AUTHORIZED_TRUE_REQUIRED")
def test_offline_node_never_queues_work(self):
self.remote["operational_transport_ready"] = False
self.assertEqual(fleet.execute_node("test-node", "printf ok", True)["error"], "NODE_OFFLINE")
self.assertEqual(fleet.node_read("test-node", "system.status", {})["error"], "NODE_OFFLINE")
if __name__ == "__main__":
unittest.main()
+144
View File
@@ -0,0 +1,144 @@
"""Legacy broker peer policy checks without sockets, desktops or live profiles."""
from __future__ import annotations
import contextlib
import io
import tempfile
import types
import unittest
from unittest import mock
from framebridge_expanded import broker
CONTROL_PATHS = (
'/fbpair/v1/control/node-status',
'/fbpair/v1/control/request',
'/fbpair/v1/control/admin-exec',
'/fbpair/v1/control/admin-cua',
'/fbpair/v1/control/operation',
'/fbpair/v1/control/result',
'/dev/authenticate-and-approve',
'/dev/approve-authenticated',
)
class BodyReadReached(Exception):
pass
class NoStoreCalls:
def __getattr__(self, name):
raise AssertionError('The peer gate must run before Store access')
class LegacyBrokerPeerTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.original = broker.original_modules()
def request_fixture(self, handler, path, peer):
request = object.__new__(handler)
request.path = path
request.client_address = (peer, 12345)
# Forwarding headers must never replace the actual transport peer.
request.headers = {'X-Forwarded-For': '127.0.0.1', 'X-Real-IP': '127.0.0.1'}
request.read_json = mock.Mock(side_effect=BodyReadReached)
request.send_json = mock.Mock()
return request
def test_default_policy_preserves_only_the_original_loopback_peer(self):
allowed = self.original.parse_control_peers()
self.assertEqual(len(allowed), 1)
self.assertTrue(self.original.control_peer_allowed('127.0.0.1', allowed))
for peer in ('192.0.2.15', '198.51.100.18', 'localhost', '', None):
with self.subTest(kind=type(peer).__name__):
self.assertFalse(self.original.control_peer_allowed(peer, allowed))
def test_explicit_literal_peers_are_canonical_and_narrow(self):
allowed = self.original.parse_control_peers(['192.0.2.15', '2001:0db8:0:0::15'])
self.assertEqual(len(allowed), 3)
self.assertTrue(self.original.control_peer_allowed('192.0.2.15', allowed))
self.assertTrue(self.original.control_peer_allowed('2001:db8::15', allowed))
self.assertTrue(self.original.control_peer_allowed('127.0.0.1', allowed))
self.assertFalse(self.original.control_peer_allowed('192.0.2.16', allowed))
def test_malformed_or_implicit_peer_configuration_is_rejected(self):
invalid = ('192.0.2.15', ['', '192.0.2.15'], ['*'], ['0.0.0.0/0'], ['0.0.0.0'], ['::'],
['example.com'], ['[::1]'], ['fe80::1%eth0'], [' 192.0.2.15'], [None], [123])
for value in invalid:
with self.subTest(configuration_type=type(value).__name__):
with self.assertRaisesRegex(ValueError, '^CONTROL_PEER_CONFIG_INVALID$'):
self.original.parse_control_peers(value)
def test_cli_rejects_invalid_configuration_before_store_or_listener(self):
with mock.patch.object(self.original, 'Store') as store, \
mock.patch.object(self.original, 'ThreadingHTTPServer') as server, \
mock.patch('sys.argv', ['legacy-broker', '--control-peer', 'example.com']), \
contextlib.redirect_stderr(io.StringIO()) as errors:
with self.assertRaises(SystemExit) as caught:
self.original.main()
self.assertEqual(caught.exception.code, 2)
self.assertIn('CONTROL_PEER_CONFIG_INVALID', errors.getvalue())
self.assertNotIn('example.com', errors.getvalue())
store.assert_not_called()
server.assert_not_called()
def test_repeated_cli_peers_reach_the_handler_only_when_explicit(self):
with mock.patch.object(self.original, 'Store'), \
mock.patch.object(self.original, 'ThreadingHTTPServer') as server, \
mock.patch.object(self.original, 'make_handler') as make_handler, \
mock.patch('sys.argv', ['legacy-broker', '--control-peer', '192.0.2.15',
'--control-peer', '2001:db8::15']), contextlib.redirect_stdout(io.StringIO()):
self.original.main()
allowed = make_handler.call_args.kwargs['control_peers']
self.assertEqual(len(allowed), 3)
self.assertTrue(self.original.control_peer_allowed('192.0.2.15', allowed))
self.assertTrue(self.original.control_peer_allowed('2001:db8::15', allowed))
server.return_value.serve_forever.assert_called_once()
def test_all_eight_routes_deny_an_unconfigured_peer_before_body_or_store(self):
handler = self.original.make_handler(NoStoreCalls(), 'http://localhost:18765')
for path in CONTROL_PATHS:
with self.subTest(path=path):
request = self.request_fixture(handler, path, '192.0.2.15')
handler.do_POST(request)
request.read_json.assert_not_called()
request.send_json.assert_called_once_with(403, {'ok': False, 'error': 'local_only'})
def test_loopback_and_explicit_peers_pass_the_gate_for_all_eight_routes(self):
handler = self.original.make_handler(NoStoreCalls(), 'http://localhost:18765',
control_peers=['192.0.2.15'])
for path in CONTROL_PATHS:
for peer in ('127.0.0.1', '192.0.2.15'):
with self.subTest(path=path, source_kind='loopback' if peer.startswith('127.') else 'explicit'):
request = self.request_fixture(handler, path, peer)
with self.assertRaises(BodyReadReached):
handler.do_POST(request)
request.read_json.assert_called_once()
def test_expanded_control_keeps_authentication_and_blocks_development_routes(self):
def no_socket_server(address, handler):
return types.SimpleNamespace(RequestHandlerClass=handler)
with tempfile.TemporaryDirectory(prefix='framebridge-broker-policy-') as state, \
mock.patch.object(broker, '_config', return_value={}), \
mock.patch.object(broker, '_control_secret', return_value='fixture-control'), \
mock.patch.object(broker, 'ThreadingHTTPServer', side_effect=no_socket_server), \
mock.patch.object(self.original, 'control_peer_allowed', side_effect=AssertionError('legacy gate reached')):
server = broker.make_server(host='localhost', state_dir=state)
handler = server.RequestHandlerClass
for path, status, error in (
('/fbpair/v1/control/nodes', 403, 'CONTROL_AUTH_REQUIRED'),
('/dev/authenticate-and-approve', 404, 'ENDPOINT_NOT_AVAILABLE'),
('/dev/approve-authenticated', 404, 'ENDPOINT_NOT_AVAILABLE'),
):
with self.subTest(path=path):
request = self.request_fixture(handler, path, '127.0.0.1')
handler.do_POST(request)
request.read_json.assert_not_called()
request.send_json.assert_called_once_with(status, {'ok': False, 'error': error})
if __name__ == '__main__':
unittest.main()
+375
View File
@@ -0,0 +1,375 @@
import hashlib
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest import mock
from framebridge_expanded import autostart, config, lifecycle, runtime, connection_background
class LifecycleContractTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.home = Path(self.tmp.name) / "home"
self.home.mkdir(mode=0o700)
self.state = self.home / "profile"
self.addCleanup(mock.patch.stopall)
mock.patch.object(Path, "home", return_value=self.home).start()
self.settings = config.default_config()
self.settings["tunnel_id"] = "tunnel_0123456789abcdef0123456789abcdef"
config.save_config(self.state, self.settings)
_, self.directory, self.unit = autostart._paths(self.state)
self.unit.parent.mkdir(mode=0o700, parents=True)
unit = autostart.unit_text(self.state, self.directory / "credential.cred").encode()
autostart._write(self.unit, unit)
cipher = b"SYNTHETIC-ENCRYPTED-FIXTURE-NOT-A-CREDENTIAL"
autostart._write(self.directory / "credential.cred", cipher)
self.record = {"schema": 1, "unit": self.unit.name,
"unit_sha256": hashlib.sha256(unit).hexdigest(),
"credential_sha256": hashlib.sha256(cipher).hexdigest(),
"connection_settings_sha256": autostart._connection_public_digest(self.settings),
"node": False, "headless_changed": False}
autostart._json(self.directory / "installation.json", self.record)
self.enabled = True
self.active = True
self.calls = []
self.system = mock.patch.object(autostart, "_systemctl", side_effect=self.systemctl).start()
self.command = mock.patch.object(autostart, "_command", side_effect=AssertionError("No OS authentication or encryption command belongs in these unit fixtures.")).start()
mock.patch.object(autostart, "_linger", return_value=True).start()
self.runtime_start = mock.patch.object(runtime, "start", return_value={"running": True, "ready": True}).start()
self.runtime_status = mock.patch.object(runtime, "status", return_value={"running": True, "ready": True}).start()
self.runtime_stop = mock.patch.object(runtime, "stop", return_value={"ok": True, "running": False}).start()
self.background_status = mock.patch.object(connection_background, "status", return_value={"ok": True, "alive": False, "verified": False, "state": "stopped"}).start()
self.background_stop = mock.patch.object(connection_background, "disconnect", return_value={"ok": True, "alive": False, "state": "stopped"}).start()
def systemctl(self, *args, **kwargs):
self.calls.append(args)
if args[0] == "enable":
self.enabled = True
if args[0] == "disable":
self.enabled = False
if args[0] == "start" or "--now" in args and args[0] == "enable":
self.active = True
if args[0] == "stop" or "--now" in args and args[0] == "disable":
self.active = False
body = ("ActiveState=" + ("active" if self.active else "inactive")
+ "\nSubState=" + ("running" if self.active else "dead")
+ "\nUnitFileState=" + ("enabled" if self.enabled else "disabled") + "\n").encode()
return subprocess.CompletedProcess(args, 0, body, b"")
def test_boot_disable_preserves_execution_cipher_and_profile(self):
paths = [self.state / "config.json", self.directory / "credential.cred", self.directory / "installation.json", self.unit]
before = {p: p.read_bytes() for p in paths}
result = lifecycle.boot_disable(self.state)
self.assertEqual("boot_disabled", result["state"])
self.assertTrue(self.active)
self.assertFalse(self.enabled)
self.assertEqual(before, {p: p.read_bytes() for p in paths})
self.assertIn(("disable", self.unit.name), self.calls)
self.runtime_stop.assert_not_called()
self.background_stop.assert_not_called()
def test_boot_enable_does_not_start_an_inactive_stack(self):
self.active = self.enabled = False
result = lifecycle.boot_enable(self.state)
self.assertTrue(result["current_execution_unchanged"])
self.assertTrue(self.enabled)
self.assertFalse(self.active)
self.assertIn(("enable", self.unit.name), self.calls)
self.runtime_start.assert_not_called()
def test_service_stop_preserves_boot_and_material(self):
before = (self.directory / "credential.cred").read_bytes()
result = lifecycle.service_stop(self.state)
self.assertTrue(result["ok"])
self.assertTrue(self.enabled)
self.assertFalse(self.active)
self.assertEqual(before, (self.directory / "credential.cred").read_bytes())
self.assertTrue(autostart.manual_stop_requested(self.state))
self.assertTrue(autostart.connection_requested(self.state))
self.assertNotIn(("disable", self.unit.name), self.calls)
def test_service_start_retains_disabled_boot_policy(self):
self.active = self.enabled = False
autostart.mark_manual_stop(self.state)
result = lifecycle.service_start(self.state)
self.assertEqual("start_requested", result["state"])
self.assertFalse(self.enabled)
self.assertTrue(self.active)
self.assertFalse(autostart.manual_stop_requested(self.state))
self.assertNotIn(("enable", self.unit.name), self.calls)
def test_disconnect_persists_intent_without_stopping_local_tools(self):
result = lifecycle.disconnect(self.state)
self.assertTrue(result["ok"])
self.assertFalse(autostart.connection_requested(self.state))
self.assertEqual("until_explicit_connect", result["scope"])
self.runtime_stop.assert_not_called()
self.assertFalse(any(x[0] in {"stop", "disable"} for x in self.calls))
self.assertEqual("disconnected", lifecycle.status(self.state)["desired_connection"])
def test_connection_intent_is_not_tied_to_a_boot_identifier(self):
lifecycle.disconnect(self.state)
record = json.loads((self.directory / "connection-intent.json").read_text())
self.assertNotIn("boot_id", record)
self.assertFalse(autostart.connection_requested(self.state))
revision = autostart.connection_request_revision(self.state)
autostart._set_connection_requested(self.state, True)
self.assertTrue(autostart.connection_requested(self.state))
self.assertGreater(autostart.connection_request_revision(self.state), revision)
def test_saved_resume_wakes_transport_without_stopping_local_tools(self):
lifecycle.disconnect(self.state)
self.background_stop.reset_mock()
self.calls.clear()
current = {"configured": True, "enabled": True, "service_active": True,
"runtime": {"running": True, "ready": True},
"connection": {"alive": False, "verified": False},
"worker": {"state": "disconnected"}}
with mock.patch.object(autostart, "status", return_value=current):
autostart.resume_connection(self.state)
self.assertTrue(autostart.connection_requested(self.state))
self.assertFalse(any(x[0] in {"stop", "start"} for x in self.calls))
self.background_stop.assert_not_called()
self.runtime_stop.assert_not_called()
self.command.assert_not_called()
def test_existing_legacy_enable_repairs_a_disabled_unit(self):
self.enabled = self.active = False
result = autostart.enable(self.state)
self.assertTrue(self.enabled)
self.assertTrue(self.active)
self.assertIn(("enable", "--now", self.unit.name), self.calls)
def test_changed_unit_is_rejected_before_boot_or_stop_actions(self):
self.unit.write_text("changed")
self.unit.chmod(0o600)
self.calls.clear()
with self.assertRaisesRegex(RuntimeError, "changed outside"):
lifecycle.boot_disable(self.state)
with self.assertRaisesRegex(RuntimeError, "changed outside"):
lifecycle.service_stop(self.state)
self.assertEqual([], self.calls)
def test_forget_removes_managed_connection_but_retains_profile_and_linger(self):
before = (self.state / "config.json").read_bytes()
autostart._set_connection_requested(self.state, False)
result = lifecycle.forget_credentials(self.state)
self.assertTrue(result["profile_preserved"])
self.assertTrue(result["linger_preserved"])
self.assertEqual(before, (self.state / "config.json").read_bytes())
self.assertFalse(self.unit.exists())
self.assertFalse((self.directory / "credential.cred").exists())
self.assertFalse((self.directory / "installation.json").exists())
self.assertFalse((self.directory / "connection-intent.json").exists())
self.assertEqual("not_configured", lifecycle.status(self.state)["desired_connection"])
self.command.assert_not_called()
def test_forget_refuses_changed_cipher_without_removing_anything(self):
p = self.directory / "credential.cred"
p.write_bytes(b"changed")
p.chmod(0o600)
self.calls.clear()
with self.assertRaisesRegex(RuntimeError, "changed outside"):
lifecycle.forget_credentials(self.state)
self.assertTrue(self.unit.exists())
self.assertTrue(p.exists())
self.assertTrue((self.directory / "installation.json").exists())
self.assertEqual([], self.calls)
def test_paused_worker_never_decrypts_or_starts_a_transport(self):
lifecycle.disconnect(self.state)
with mock.patch.object(autostart, "_credential") as credential, \
mock.patch.object(connection_background, "start") as launch, \
mock.patch.object(autostart.time, "sleep", side_effect=lambda _: setattr(autostart, "_STOP", True)):
self.assertEqual(0, autostart.worker(self.state))
credential.assert_not_called()
launch.assert_not_called()
self.assertFalse(autostart.connection_requested(self.state))
def test_explicit_connect_resumes_paused_worker_without_runtime_restart(self):
lifecycle.disconnect(self.state)
sleeps = 0
def sleep(_):
nonlocal sleeps
sleeps += 1
if sleeps == 1:
autostart._set_connection_requested(self.state, True)
else:
autostart._STOP = True
with mock.patch.object(autostart, "_credential", return_value="SYNTHETIC-NOT-A-REAL-KEY") as credential, \
mock.patch.object(connection_background, "start") as launch, \
mock.patch.object(autostart.time, "sleep", side_effect=sleep):
self.assertEqual(0, autostart.worker(self.state))
self.assertEqual(1, launch.call_count)
self.assertEqual(1, credential.call_count)
self.assertEqual(1, self.runtime_start.call_count)
self.assertEqual(1, self.runtime_stop.call_count) # Final simulated unit stop only.
def test_legacy_current_only_disconnect_is_not_restarted_in_same_worker(self):
sleeps = 0
def sleep(_):
nonlocal sleeps
sleeps += 1
if sleeps == 2:
autostart._STOP = True
states = [{"alive": False, "state": "stopped"},
{"alive": True, "verified": True, "state": "ready"},
{"alive": False, "state": "stopped"}]
self.background_status.side_effect = states
with mock.patch.object(autostart, "_credential", return_value="SYNTHETIC-NOT-A-REAL-KEY"), \
mock.patch.object(connection_background, "start") as launch, \
mock.patch.object(autostart.time, "sleep", side_effect=sleep):
self.assertEqual(0, autostart.worker(self.state))
self.assertEqual(1, launch.call_count)
self.assertEqual(1, self.runtime_start.call_count)
def test_transport_error_retries_without_restarting_local_runtime(self):
self.background_status.return_value = {"alive": False, "state": "error"}
sleeps = 0
clock = [0.0]
def sleep(_):
nonlocal sleeps
sleeps += 1
clock[0] += 15
if sleeps == 2:
autostart._STOP = True
with mock.patch.object(autostart, "_credential", return_value="SYNTHETIC-NOT-A-REAL-KEY"), \
mock.patch.object(connection_background, "start") as launch, \
mock.patch.object(autostart.time, "sleep", side_effect=sleep), \
mock.patch.object(autostart.time, "monotonic", side_effect=lambda: clock[0]):
self.assertEqual(0, autostart.worker(self.state))
self.assertEqual(2, launch.call_count)
self.assertEqual(1, self.runtime_start.call_count)
def test_connect_respects_an_explicit_disabled_boot_policy(self):
lifecycle.boot_disable(self.state)
self.calls.clear()
current = {"configured": True, "enabled": False, "service_active": True,
"runtime": {"running": True, "ready": True},
"connection": {"alive": False, "verified": False},
"worker": {"state": "disconnected"}}
with mock.patch.object(autostart, "status", return_value=current):
autostart.resume_connection(self.state)
self.assertFalse(autostart.boot_requested(self.state))
self.assertFalse(any(x[0] == "enable" for x in self.calls))
def test_connect_starts_disabled_inactive_service_without_enabling_boot(self):
lifecycle.boot_disable(self.state)
self.active = False
self.calls.clear()
current = {"configured": True, "enabled": False, "service_active": False,
"runtime": {"running": False, "ready": False}, "worker": {"state": "stopped"}}
with mock.patch.object(autostart, "status", return_value=current):
autostart.resume_connection(self.state)
self.assertIn(("start", self.unit.name), self.calls)
self.assertFalse(self.enabled)
self.assertFalse(any(x[0] == "enable" for x in self.calls))
def test_failed_boot_policy_action_restores_previous_intent(self):
lifecycle.boot_enable(self.state)
before = (self.directory / "boot-policy.json").read_bytes()
with mock.patch.object(autostart, "_systemctl", side_effect=RuntimeError("synthetic service failure")):
with self.assertRaisesRegex(RuntimeError, "synthetic"):
lifecycle.boot_disable(self.state)
self.assertEqual(before, (self.directory / "boot-policy.json").read_bytes())
self.assertTrue(autostart.boot_requested(self.state))
def _new_node(self):
state = self.home / "new-node"
settings = config.default_config(role="node")
settings.update(headless="auto", broker_url="https://example.test:20878")
config.save_config(state, settings)
self.active = self.enabled = False
def command(args, **kwargs):
return subprocess.CompletedProcess(args, 0, b"systemd 257\n" if args[:2] == ["systemd-creds", "--version"] else b"", b"")
self.command.side_effect = command
return state
def test_initial_node_service_start_needs_no_chatgpt_key_or_extra_question(self):
state = self._new_node()
with mock.patch("builtins.input", side_effect=AssertionError("No redundant startup question")), \
mock.patch.object(autostart.getpass, "getpass", side_effect=AssertionError("Node has no ChatGPT key")), \
mock.patch.object(autostart, "_enabled_units", return_value=[]):
result = lifecycle.service_start(state)
_, directory, unit = autostart._paths(state)
self.assertTrue(result["initial_configuration"])
self.assertTrue(result["boot_policy_changed"])
self.assertIn(("enable", "--now", unit.name), self.calls)
self.assertFalse((directory / "credential.cred").exists())
self.assertTrue(json.loads((directory / "installation.json").read_text())["node"])
def test_initial_node_boot_enable_does_not_start_current_execution(self):
state = self._new_node()
with mock.patch("builtins.input", side_effect=AssertionError("No redundant startup question")), \
mock.patch.object(autostart.getpass, "getpass", side_effect=AssertionError("Node has no ChatGPT key")), \
mock.patch.object(autostart, "_enabled_units", return_value=[]):
result = lifecycle.boot_enable(state)
_, directory, unit = autostart._paths(state)
self.assertTrue(result["initial_configuration"])
self.assertTrue(result["current_execution_unchanged"])
self.assertTrue(autostart.boot_requested(state))
self.assertFalse(self.active)
self.assertNotIn(("enable", "--now", unit.name), self.calls)
self.assertFalse((directory / "credential.cred").exists())
def test_status_handles_a_profile_with_an_empty_autostart_directory(self):
state = self.home / "not-configured"
config.save_config(state, config.default_config())
(state / "autostart").mkdir(mode=0o700)
result = lifecycle.status(state)
self.assertTrue(result["ok"])
self.assertFalse(result["boot"]["configured"])
self.assertEqual("not_configured", result["desired_connection"])
self.assertEqual("not_configured", autostart.status(state)["desired_connection"])
def test_persistent_transport_error_respects_retry_delay(self):
self.background_status.return_value = {"alive": False, "state": "error"}
sleeps = 0
def sleep(_):
nonlocal sleeps
sleeps += 1
if sleeps == 2:
autostart._STOP = True
with mock.patch.object(autostart, "_credential", return_value="SYNTHETIC-NOT-A-REAL-KEY"), \
mock.patch.object(connection_background, "start") as launch, \
mock.patch.object(autostart.time, "sleep", side_effect=sleep), \
mock.patch.object(autostart.time, "monotonic", return_value=0.0):
self.assertEqual(0, autostart.worker(self.state))
self.assertEqual(1, launch.call_count)
self.assertEqual(1, self.runtime_start.call_count)
def test_service_stop_does_not_confuse_query_success_with_termination(self):
results = [{"ok": True, "running": True, "processes": []},
{"ok": True, "running": False, "processes": [{"alive": True}]}]
for observed in results:
with self.subTest(observed=observed), \
mock.patch.object(runtime, "stop", return_value=observed), \
mock.patch.object(lifecycle.time, "monotonic", side_effect=[0.0, 3.0]):
result = lifecycle.service_stop(self.state)
self.assertFalse(result["ok"])
self.assertEqual("stop_incomplete", result["state"])
self.assertTrue((self.directory / "credential.cred").exists())
def test_forget_retains_material_if_local_tools_remain_running(self):
before = (self.directory / "credential.cred").read_bytes()
with mock.patch.object(runtime, "stop", return_value={"ok": True, "running": True}), \
mock.patch.object(lifecycle.time, "monotonic", side_effect=[0.0, 3.0]):
with self.assertRaisesRegex(RuntimeError, "material was retained"):
lifecycle.forget_credentials(self.state)
self.assertEqual(before, (self.directory / "credential.cred").read_bytes())
self.assertTrue(self.unit.exists())
self.assertTrue((self.directory / "installation.json").exists())
if __name__ == "__main__":
unittest.main()
+203
View File
@@ -0,0 +1,203 @@
"""Dispatch-race and uncertain-outcome tests using inert CUA engines only."""
import json
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from framebridge_expanded import display_context as dc
from framebridge_expanded import native_host as nh
from framebridge_expanded import adapter as ad
class CuaError(RuntimeError):
def __init__(self, code, message=None):
self.code = code
super().__init__(message or code)
class NativeDispatchTests(unittest.TestCase):
def fake(self, calls, action=None):
cua = SimpleNamespace(CuaTransportError=CuaError)
def run(argv, timeout=15):
calls.append(list(argv))
return SimpleNamespace(returncode=0, stdout="", stderr="")
cua._run = run
cua.handle_cua_message = lambda msg: action(cua) if action else {"ok": True}
return cua
def checker(self, fail_at):
state = {"count": 0}
def check(env):
state["count"] += 1
if state["count"] >= fail_at:
raise dc.DisplayContextError("CUA_FOREGROUND_SESSION_CHANGED")
return {}
return check
def test_queue_then_context_change_blocks_first_primitive(self):
calls = []
cua = self.fake(calls, lambda engine: engine._run(["xdotool", "key", "a"]))
state = nh.install_cua_guard(cua, env={}, checker=self.checker(2))
with self.assertRaises(CuaError):
cua.handle_cua_message({})
self.assertEqual(calls, [])
result = nh.decorate_response({"executed": False}, state, {})
self.assertFalse(result["input_may_have_been_sent"])
self.assertFalse(result["performed"])
self.assertFalse(result["do_not_replay"])
def test_context_change_between_query_and_input_blocks_input(self):
calls = []
def action(cua):
cua._run(["xdotool", "getactivewindow"])
cua._run(["xdotool", "click", "1"])
cua = self.fake(calls, action)
state = nh.install_cua_guard(cua, env={}, checker=self.checker(3))
with self.assertRaises(CuaError):
cua.handle_cua_message({})
self.assertEqual(calls, [["xdotool", "getactivewindow"]])
self.assertFalse(state["input_started"])
def test_postflight_failure_after_input_retains_uncertain_outcome(self):
calls = []
cua = self.fake(calls, lambda engine: engine._run(["xdotool", "type", "x"]))
state = nh.install_cua_guard(cua, env={}, checker=self.checker(3))
with self.assertRaises(CuaError):
cua.handle_cua_message({})
result = nh.decorate_response({"executed": True, "performed": True}, state, {})
self.assertFalse(result["executed"])
self.assertTrue(result["input_may_have_been_sent"])
self.assertTrue(result["do_not_replay"])
self.assertNotEqual(result.get("performed"), False)
self.assertTrue(json.loads(result["output"])["result_discarded"])
def test_observe_late_failure_does_not_claim_input(self):
calls = []
cua = self.fake(calls, lambda engine: engine._run(["import", "-window", "root", "fixture.png"]))
state = nh.install_cua_guard(cua, env={}, checker=self.checker(3))
with self.assertRaises(CuaError):
cua.handle_cua_message({})
result = nh.decorate_response({"executed": False}, state, {})
self.assertFalse(result["input_may_have_been_sent"])
self.assertFalse(result["performed"])
def test_successful_input_is_reported_performed(self):
calls = []
cua = self.fake(calls, lambda engine: engine._run(["/usr/bin/xdotool", "key", "a"]))
state = nh.install_cua_guard(cua, env={}, checker=lambda env: {})
cua.handle_cua_message({})
result = nh.decorate_response({"executed": True}, state, {})
self.assertTrue(result["performed"])
self.assertTrue(result["input_may_have_been_sent"])
self.assertFalse(result["do_not_replay"])
def test_captured_pointer_helper_rechecks_after_its_internal_query(self):
calls = []
cua = self.fake(calls)
cua._run_v3_base = cua._run
def helper(argv, timeout=15):
cua._run_v3_base(["xdotool", "getmouselocation", "--shell"])
return cua._run_v3_base(argv, timeout=timeout)
cua._run = helper
cua.handle_cua_message = lambda msg: cua._run(["xdotool", "mousemove", "1", "1"])
state = nh.install_cua_guard(cua, env={}, checker=self.checker(4))
with self.assertRaises(CuaError):
cua.handle_cua_message({})
self.assertEqual(calls, [["xdotool", "getmouselocation", "--shell"]])
self.assertFalse(state["input_started"])
def test_metadata_does_not_leak_to_next_non_cua_response(self):
state = {"active": True, "input_started": True, "failed_code": ""}
first = nh.decorate_response({"executed": True}, state, {})
self.assertTrue(first["input_may_have_been_sent"])
second = {"executed": True, "channel": "CONTROL-DIRECT"}
self.assertEqual(nh.decorate_response(second, state, {}), second)
self.assertNotIn("input_may_have_been_sent", second)
class NativeFramingTests(unittest.TestCase):
def test_captured_host_protocol_rejects_unbound_display_without_input(self):
root = Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory(prefix="fb-native-guard-") as home:
env = {"HOME": home, "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
"PYTHONDONTWRITEBYTECODE": "1", "LC_ALL": "C.UTF-8",
"FRAMEBRIDGE_EXPANDED_PREFIX": str(root), dc.BINDING: "{}"}
message = json.dumps({"op": "cua", "action": "OBSERVE", "authorized": "YES",
"target": "ASUS", "id": "r2-native-test"}).encode()
proc = subprocess.run([sys.executable, str(root / "packaging/framebridge-native-expanded")],
input=struct.pack("<I", len(message)) + message,
env=env, capture_output=True, timeout=15)
self.assertEqual(proc.returncode, 0, proc.stderr.decode(errors="replace")[-500:])
self.assertGreaterEqual(len(proc.stdout), 4)
length = struct.unpack("<I", proc.stdout[:4])[0]
self.assertEqual(len(proc.stdout), length + 4)
result = json.loads(proc.stdout[4:])
self.assertFalse(result["executed"])
self.assertFalse(result["performed"])
self.assertFalse(result["input_may_have_been_sent"])
self.assertFalse(result["do_not_replay"])
self.assertEqual(json.loads(result["output"])["error_code"], "CUA_DISPLAY_UNAVAILABLE")
class AdapterDispatchTests(unittest.TestCase):
def adapter(self, response=None, error=None):
adapter = SimpleNamespace()
def roundtrip(payload, timeout_s=60):
if error:
raise error
return dict(response or {"executed": True})
def handle(request, job_id):
native = adapter.native_cua_roundtrip(request["cua"], timeout_s=60)
return {"ok": bool(native.get("executed")), "mode": "CUA"}
adapter.native_cua_roundtrip = roundtrip
adapter.handle_cua_request = handle
return adapter
def test_native_metadata_survives_original_adapter_mapping(self):
adapter = self.adapter({"executed": False, "input_may_have_been_sent": True,
"do_not_replay": True})
ad.install_adapter_guard(adapter, env={}, checker=lambda env: {})
result = adapter.handle_cua_request({"cua": {"action": "KEY"}}, "fixture")
self.assertTrue(result["input_may_have_been_sent"])
self.assertTrue(result["do_not_replay"])
def test_before_dispatch_failure_claims_no_input(self):
adapter = self.adapter()
def check(env):
raise dc.DisplayContextError("CUA_FOREGROUND_SESSION_CHANGED")
ad.install_adapter_guard(adapter, env={}, checker=check)
result = adapter.handle_cua_request({"cua": {"action": "KEY"}}, "fixture")
self.assertFalse(result["performed"])
self.assertFalse(result["do_not_replay"])
def test_timeout_after_dispatch_never_invites_input_replay(self):
adapter = self.adapter(error=TimeoutError("native transport timed out"))
ad.install_adapter_guard(adapter, env={}, checker=lambda env: {})
result = adapter.handle_cua_request({"cua": {"action": "KEY"}}, "fixture")
self.assertFalse(result["ok"])
self.assertTrue(result["input_may_have_been_sent"])
self.assertTrue(result["do_not_replay"])
self.assertNotIn("performed", result)
def test_observe_timeout_has_no_input_outcome(self):
adapter = self.adapter(error=TimeoutError("native transport timed out"))
ad.install_adapter_guard(adapter, env={}, checker=lambda env: {})
result = adapter.handle_cua_request({"cua": {"action": "OBSERVE"}}, "fixture")
self.assertFalse(result["input_may_have_been_sent"])
self.assertFalse(result["performed"])
def test_adapter_postflight_preserves_explicit_no_input(self):
adapter = self.adapter({"executed": False, "input_may_have_been_sent": False})
count = [0]
def check(env):
count[0] += 1
if count[0] == 2:
raise dc.DisplayContextError("CUA_FOREGROUND_SESSION_CHANGED")
ad.install_adapter_guard(adapter, env={}, checker=check)
result = adapter.handle_cua_request({"cua": {"action": "KEY"}}, "fixture")
self.assertFalse(result["input_may_have_been_sent"])
self.assertFalse(result["performed"])
if __name__ == "__main__":
unittest.main()
+83
View File
@@ -0,0 +1,83 @@
import unittest
from pathlib import Path
import tempfile
from unittest import mock
from framebridge_expanded import network
def interface(name="eth0", *, address=True, flags=None, scope="global"):
return {"ifname": name, "flags": ["UP", "LOWER_UP"] if flags is None else flags,
"operstate": "UP", "addr_info": [
{"family": "inet", "local": "192.0.2.20", "prefixlen": 24, "scope": scope}
] if address else []}
def route(name="eth0"):
return {"dst": "default", "dev": name, "gateway": "192.0.2.1"}
class NetworkTests(unittest.TestCase):
def test_working_ifupdown_is_not_misreported_from_nm_unmanaged(self):
value = network.summarize([interface()], {"ipv4": [route()]},
{"eth0": {"managed": False, "state": "10 (unmanaged)"}}, {"eth0"})
self.assertEqual(value["connectivity"], "LINK_ADDRESS_DEFAULT_ROUTE")
self.assertEqual(value["interfaces"][0]["manager"], "ifupdown")
self.assertFalse(value["internet_verified"])
self.assertEqual(value["warnings"][0]["code"], "CONNECTED_INTERFACE_NOT_MANAGED_BY_NETWORKMANAGER")
def test_manager_connected_state_does_not_substitute_for_an_address(self):
value = network.summarize([interface(address=False)], {"ipv4": [route()]},
{"eth0": {"managed": True, "state": "100 (connected)"}})
self.assertEqual(value["connectivity"], "NOT_ESTABLISHED")
def test_address_without_default_route_is_not_internet_claim(self):
value = network.summarize([interface()], {"ipv4": []})
self.assertEqual(value["connectivity"], "NOT_ESTABLISHED")
self.assertFalse(value["internet_verified"])
def test_loopback_does_not_establish_external_connectivity(self):
value = network.summarize([interface("lo")], {"ipv4": [route("lo")]},
{"lo": {"managed": True, "state": "100 (connected (externally))"}})
self.assertEqual(value["connectivity"], "NOT_ESTABLISHED")
def test_interface_down_is_not_ready_even_with_residual_routes(self):
value = network.summarize([interface(flags=[])], {"ipv4": [route()]})
self.assertFalse(value["interfaces"][0]["local_route_ready"])
def test_ipv6_global_address_and_route_are_supported(self):
row = interface()
row["addr_info"] = [{"family": "inet6", "local": "2001:db8::20", "prefixlen": 64, "scope": "global"}]
value = network.summarize([row], {"ipv6": [{"dst": "default", "dev": "eth0", "gateway": "fe80::1"}]})
self.assertEqual(value["connectivity"], "LINK_ADDRESS_DEFAULT_ROUTE")
def test_unmanaged_is_not_automatically_named_ifupdown(self):
value = network.summarize([interface()], {"ipv4": [route()]},
{"eth0": {"managed": False, "state": "10 (unmanaged)"}})
self.assertEqual(value["interfaces"][0]["manager"], "unverified")
def test_missing_kernel_data_returns_explicit_failure(self):
with mock.patch.object(network, "_json", return_value=None), \
mock.patch.object(network.shutil, "which", return_value=None), \
mock.patch.object(network, "_active", return_value=False):
value = network.inspect()
self.assertFalse(value["ok"])
self.assertEqual(value["error"], "NETWORK_KERNEL_STATUS_UNAVAILABLE")
self.assertFalse(value["internet_verified"])
def test_ifupdown_state_contains_names_only_and_rejects_symlinks(self):
with tempfile.TemporaryDirectory() as path:
state = Path(path) / "ifstate"
state.write_text("lo=lo\neth0=eth0\nunsafe command=value\n")
state.chmod(0o600)
self.assertEqual(network._ifupdown(state), {"lo", "eth0"})
link = Path(path) / "link"
link.symlink_to(state)
self.assertEqual(network._ifupdown(link), set())
def test_nm_parser_accepts_only_device_metadata(self):
output = ("GENERAL.DEVICE:eth0\nGENERAL.STATE:10 (unmanaged)\nGENERAL.NM-MANAGED:no\n"
"GENERAL.DEVICE:eth\\:bad\nGENERAL.STATE:100 (connected)\nGENERAL.NM-MANAGED:yes\n")
with mock.patch.object(network, "_command", return_value=(0, output)) as command:
devices = network._nm_devices()
self.assertEqual(devices, {"eth0": {"managed": False, "state": "10 (unmanaged)"}})
self.assertNotIn("connection", command.call_args.args[0])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,51 @@
"""Exercise the actual portable CUA routing function without importing its MCP server."""
import ast
import copy
from pathlib import Path
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
def actual_cua(screen, dispatch):
source = ROOT / "portable/front_door_mcp.py"
tree = ast.parse(source.read_text())
function = copy.deepcopy(next(node for node in tree.body
if isinstance(node, ast.FunctionDef) and node.name == "framebridge_cua"))
function.decorator_list = []
module = ast.fix_missing_locations(ast.Module(body=[function], type_ignores=[]))
namespace = {"_real_screen_client": screen, "dispatch_cua_request": dispatch}
exec(compile(module, str(source), "exec"), namespace)
return namespace["framebridge_cua"]
class PortableCaptureScopeTests(unittest.TestCase):
def test_node_scope_preserves_alias_case_and_uses_real_screen_client(self):
screen = mock.Mock()
screen.call.return_value = {"fixture": "actual-client-route-only"}
dispatch = mock.Mock(side_effect=AssertionError("legacy CUA is not the physical node route"))
cua = actual_cua(screen, dispatch)
value = cua("observe", True, capture_scope=" NoDe:CaseSensitive-Node ", target="UNRELATED")
self.assertEqual(value, screen.call.return_value)
screen.call.assert_called_once_with("observe", "CaseSensitive-Node", True)
dispatch.assert_not_called()
def test_node_scope_input_is_rejected_without_hidden_or_graphical_fallback(self):
for action in ["TYPE", "KEY", "CLICK", "DRAG"]:
with self.subTest(action=action):
screen, dispatch = mock.Mock(), mock.Mock()
cua = actual_cua(screen, dispatch)
value = cua(action, True, capture_scope="node:CaseSensitive-Node", text="not executed")
self.assertEqual(value["error"], "NODE_SCREEN_SCOPE_OBSERVE_ONLY")
screen.call.assert_not_called()
dispatch.assert_not_called()
def test_authorization_precedes_physical_capture_dispatch(self):
screen, dispatch = mock.Mock(), mock.Mock()
cua = actual_cua(screen, dispatch)
value = cua("OBSERVE", False, capture_scope="node:CaseSensitive-Node")
self.assertEqual(value["error"], "AUTHORIZED_TRUE_REQUIRED")
screen.call.assert_not_called()
dispatch.assert_not_called()
if __name__ == "__main__":
unittest.main()
+170
View File
@@ -0,0 +1,170 @@
"""Regression checks for portable helpers and removed implicit lab routing."""
from __future__ import annotations
import ast
import importlib.util
import json
import os
from pathlib import Path
import re
import subprocess
import tempfile
import types
import unittest
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1]
FRONT_DOORS = (
SOURCE / 'portable/gerameg_cua_frontdoor.py',
SOURCE / 'sources/original-geranas/FrameBridge-Front-Door/bin/gerameg_cua_frontdoor.py',
)
RPC = SOURCE / 'sources/original-gerameg/cua_tty_rpc.py'
HOST = SOURCE / 'sources/original-asus/framebridge-native/host/framebridge_host.py'
LAUNCHER = SOURCE / 'packaging/framebridge-original'
PROGRESS_CONSOLE = SOURCE / 'sources/original-asus/framebridge-native/host/open-progress-console.sh'
def load_module(path, name):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class PublicReleasePrivacyTests(unittest.TestCase):
def test_captured_journal_uses_account_state_without_payload_contents(self):
# Invoke only the journal writer with synthetic metadata in a temporary HOME.
tree = ast.parse(HOST.read_text())
journal = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == '_cua_runtime_journal')
namespace = {'Path': Path, 'json': json}
exec(compile(ast.Module(body=[journal], type_ignores=[]), str(HOST), 'exec'), namespace)
with tempfile.TemporaryDirectory(prefix='framebridge-journal-') as temporary:
home = Path(temporary) / 'different account'
with mock.patch.object(Path, 'home', return_value=home):
namespace['_cua_runtime_journal']('synthetic_event',
{'id': 'example-request', 'action': 'STATUS',
'script': 'private-example-input-not-to-log'}, rc=0)
log = home / '.local/state/framebridge/cua-runtime/cua-runtime.log'
records = log.read_text().splitlines()
self.assertEqual(len(records), 1)
record = json.loads(records[0])
self.assertEqual(record['event'], 'synthetic_event')
self.assertEqual(record['id'], 'example-request')
self.assertNotIn('private-example-input-not-to-log', records[0])
self.assertEqual([path.name for path in home.iterdir()], ['.local'])
def test_captured_launcher_keeps_progress_inside_generic_profile_state(self):
# settings() computes paths; no process or UI is started by this check.
import hashlib
tree = ast.parse(LAUNCHER.read_text())
settings = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == 'settings')
with tempfile.TemporaryDirectory(prefix='framebridge-profile-') as temporary:
home = Path(temporary) / 'different account'
state = home / 'selected profile'
namespace = {'Path': Path, 'os': os, 'hashlib': hashlib,
'REAL_HOME': home, 'PREFIX': Path('/opt/framebridge-original'),
'LAYOUT': {'fbterm_bin': 'fbterm', 'native_host': 'host.py'}}
exec(compile(ast.Module(body=[settings], type_ignores=[]), str(LAUNCHER), 'exec'), namespace)
args = types.SimpleNamespace(state_dir=str(state), port=12345, hub_port=12346, broker_port=12347)
with mock.patch.dict(os.environ, {'HOME': str(home)}, clear=True):
actual_state, profile_home, environment = namespace['settings'](args)
self.assertEqual(actual_state, state)
self.assertEqual(profile_home, state / 'home')
progress = Path(environment['FRAMEBRIDGE_PROGRESS_DIR'])
self.assertEqual(progress, profile_home / '.local/state/framebridge/progress')
self.assertEqual(Path(environment['FRAMEBRIDGE_PROGRESS_LOG']), progress / 'framebridge-progress.log')
self.assertFalse(state.exists())
def test_progress_console_uses_account_state_and_honors_explicit_overrides(self):
# Execute the two variable assignments only, never the graphical launcher.
assignments = [line for line in PROGRESS_CONSOLE.read_text().splitlines()
if line.startswith(('LOG_DIR=', 'LOG='))]
self.assertEqual(len(assignments), 2)
script = '\n'.join(assignments) + '\nprintf "%s\\0%s" "$LOG_DIR" "$LOG"\n'
with tempfile.TemporaryDirectory(prefix='framebridge-progress-') as temporary:
home = Path(temporary) / 'different account'
cases = [({}, home / '.local/state/framebridge/progress', None),
({'FRAMEBRIDGE_PROGRESS_DIR': str(home / 'custom directory')}, home / 'custom directory', None),
({'FRAMEBRIDGE_PROGRESS_LOG': str(home / 'specific file.log')},
home / '.local/state/framebridge/progress', home / 'specific file.log')]
for overrides, expected_directory, expected_file in cases:
with self.subTest(overrides=tuple(overrides)):
completed = subprocess.run(['/bin/bash', '-c', script],
env={'HOME': str(home), **overrides}, capture_output=True, timeout=10, check=True)
directory, log = completed.stdout.decode().split('\0')
self.assertEqual(Path(directory), expected_directory)
self.assertEqual(Path(log), expected_file or expected_directory / 'framebridge-progress.log')
self.assertFalse(home.exists())
def test_legacy_rpc_resolves_home_in_execution_account_and_quotes_arguments(self):
# Execute only our synthetic stub, not the captured CUA client or server.
with tempfile.TemporaryDirectory(prefix='framebridge-public-check-') as temporary:
remote_home = Path(temporary) / 'different user home'
stub = remote_home / '.local/lib/framebridge-gerameg-original/cua_tty_rpc.py'
stub.parent.mkdir(parents=True)
stub.write_text('import json, os, sys\nprint(json.dumps({"home": os.environ["HOME"], "args": sys.argv[1:]}))\n')
marker = Path(temporary) / 'must-not-exist'
arguments = ['request', 'spaces and quotes \' "', '$(touch ' + str(marker) + ')', '; false']
for index, path in enumerate(FRONT_DOORS):
with self.subTest(front_door=index):
module = load_module(path, 'public_front_door_' + str(index))
with mock.patch.object(Path, 'home', return_value=Path('/unrelated/central/home')):
command = module._remote_script(arguments)
completed = subprocess.run(['/bin/sh', '-c', command],
env={**os.environ, 'HOME': str(remote_home)},
text=True, capture_output=True, timeout=10, check=True)
result = json.loads(completed.stdout)
self.assertEqual(result['home'], str(remote_home))
self.assertEqual(result['args'], arguments)
self.assertFalse(marker.exists())
def test_legacy_rpc_requires_a_remote_home(self):
for index, path in enumerate(FRONT_DOORS):
module = load_module(path, 'public_front_door_no_home_' + str(index))
environment = dict(os.environ)
environment.pop('HOME', None)
completed = subprocess.run(['/bin/sh', '-c', module._remote_script(['request'])],
env=environment, text=True, capture_output=True, timeout=10)
self.assertNotEqual(completed.returncode, 0)
def test_captured_rpc_uses_accounts_home_and_retains_identity_guard(self):
with tempfile.TemporaryDirectory(prefix='framebridge-owner-home-') as temporary:
home = Path(temporary)
with mock.patch.object(Path, 'home', return_value=home):
module = load_module(RPC, 'public_captured_rpc')
self.assertEqual(module.HOME, home)
with mock.patch.object(module.os, 'getuid', return_value=module.UID), \
mock.patch.object(module.os, 'geteuid', return_value=module.UID), \
mock.patch.object(module.socket, 'gethostname', return_value=module.HOSTNAME):
module.guard_runtime()
with mock.patch.object(module.os, 'geteuid', return_value=module.UID + 1):
with self.assertRaisesRegex(module.RpcError, 'UID_MISMATCH'):
module.guard_runtime()
with mock.patch.object(Path, 'home', return_value=home / 'other'):
with self.assertRaisesRegex(module.RpcError, 'HOME_MISMATCH'):
module.guard_runtime()
def test_no_implicit_lab_node_is_reachable_through_classic_routes(self):
# Compile the early-routing functions only: no native host launch or CUA.
tree = ast.parse(HOST.read_text())
selected = []
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == 'CLASSIC_NODE_TARGETS' for t in node.targets):
selected.append(node)
if isinstance(node, ast.FunctionDef) and node.name in ('route_classic_node', 'route_classic_plan'):
selected.append(node)
namespace = {'NODE_TARGET_RE': re.compile(r'[A-Z0-9_-]+')}
exec(compile(ast.Module(body=selected, type_ignores=[]), str(HOST), 'exec'), namespace)
self.assertEqual(namespace['CLASSIC_NODE_TARGETS'], {})
for target in ('FRESH-MANUAL', 'USER-WORKER'):
message = {'target': target, 'mode': 'EXEC', 'lang': 'BASH', 'script': 'printf example'}
result = namespace['route_classic_node'](message)
self.assertFalse(result['executed'])
self.assertEqual(result['error'], 'CLASSIC_NODE_TARGET_UNKNOWN')
result = namespace['route_classic_plan'](message)
self.assertFalse(result['executed'])
self.assertEqual(result['error'], 'NODEPLAN_TARGET_UNKNOWN')
if __name__ == '__main__':
unittest.main()
+76
View File
@@ -0,0 +1,76 @@
"""Exercise the real start preflight with disposable TCP sockets, without spawning services."""
import errno
from pathlib import Path
import socket
import sys
import tempfile
import unittest
from unittest.mock import patch
from framebridge_expanded import runtime
class SpawnBoundaryReached(Exception):
pass
@unittest.skipUnless(sys.platform.startswith("linux"), "Linux TCP restart regression")
class RuntimePortPreflightTests(unittest.TestCase):
def start_until_spawn(self, state, ports):
settings = {"role": "central", "ports": ports}
with patch.object(runtime, "state_path", return_value=state), \
patch.object(runtime, "load_config", return_value=settings), \
patch.object(runtime, "status", return_value={"running": False}), \
patch.object(runtime, "doctor", return_value={"ok": True}), \
patch.object(runtime, "environment", return_value={}), \
patch.object(runtime, "_spawn", side_effect=SpawnBoundaryReached) as spawn:
with self.assertRaises(SpawnBoundaryReached):
runtime.start(state)
spawn.assert_called_once()
def close_server_first(self):
with socket.socket() as listener, socket.socket() as client:
listener.settimeout(2)
client.settimeout(2)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1]
listener.listen(1)
client.connect(("127.0.0.1", port))
with listener.accept()[0] as accepted:
accepted.settimeout(2)
accepted.shutdown(socket.SHUT_WR)
self.assertEqual(client.recv(1), b"")
client.close()
self.assertEqual(accepted.recv(1), b"")
return port
def test_closed_connections_do_not_block_central_restart(self):
ports = {name: self.close_server_first() for name in ("mcp", "hub", "broker")}
self.assertEqual(len(set(ports.values())), 3)
# Establish that each port has the regression condition, not an active listener.
for port in ports.values():
with socket.socket() as original_probe:
with self.assertRaises(OSError) as blocked:
original_probe.bind(("127.0.0.1", port))
self.assertEqual(blocked.exception.errno, errno.EADDRINUSE)
with tempfile.TemporaryDirectory() as directory:
self.start_until_spawn(Path(directory), ports)
def test_active_loopback_and_wildcard_listeners_still_block_start(self):
for address in ("127.0.0.1", "0.0.0.0"):
with self.subTest(address=address), socket.socket() as listener, \
tempfile.TemporaryDirectory() as directory:
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind((address, 0))
listener.listen(1)
port = listener.getsockname()[1]
# The first preflight port is occupied, so no process may be spawned.
ports = {name: port for name in ("mcp", "hub", "broker")}
with self.assertRaises(OSError) as blocked:
self.start_until_spawn(Path(directory), ports)
self.assertEqual(blocked.exception.errno, errno.EADDRINUSE)
if __name__ == "__main__":
unittest.main()
+346
View File
@@ -0,0 +1,346 @@
"""Framebuffer policy regressions: no real device, ACL, udev or privilege changes."""
import contextlib
from contextlib import ExitStack
import io
from pathlib import Path
import stat
import struct
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import cli, screen_access as sa
def acl(entries):
return struct.pack("<I", 2) + b"".join(struct.pack("<HHI", *item) for item in entries)
U = sa.UNDEFINED
BASE = [(1, 6, U), (4, 6, U), (32, 0, U)]
EXTENDED = [(1, 6, U), (2, 7, 1001), (4, 6, U), (8, 2, 99), (16, 6, U), (32, 0, U)]
META = {"dev": 1, "inode": 2, "rdev": 7424, "uid": 0, "gid": 44,
"mode": 0o660, "boot": "test-boot"}
POLICY = {"schema": 1, "enabled": True, "device": "/dev/fb0", "user": "fbtest", "uid": 1000}
class AclPlanTests(unittest.TestCase):
def test_plain_mode_adds_only_named_read_and_same_mask(self):
result = sa.acl_entries(sa.read_only_acl(None, 0o660, 1000), 0o660)
self.assertEqual(result, [(1, 6, U), (2, 4, 1000), (4, 6, U), (16, 6, U), (32, 0, U)])
def test_existing_acl_other_users_groups_mask_unchanged(self):
original = acl(EXTENDED)
result = sa.acl_entries(sa.read_only_acl(original, 0o660, 1000), 0o660)
self.assertEqual([e for e in result if e != (2, 4, 1000)], EXTENDED)
self.assertEqual(next(p for t, p, u in result if t == 16), 6)
def test_mask_without_read_is_rejected_instead_of_broadened(self):
raw = acl([(1, 6, U), (2, 7, 1001), (4, 6, U), (16, 2, U), (32, 0, U)])
with self.assertRaisesRegex(RuntimeError, "ACL_MASK_HAS_NO_READ"):
sa.read_only_acl(raw, 0o620, 1000)
with self.assertRaisesRegex(RuntimeError, "ACL_MASK_HAS_NO_READ"):
sa.read_only_acl(None, 0o600, 1000)
def test_existing_read_acl_is_unmodified_and_not_claimed(self):
raw = acl([(1, 6, U), (2, 4, 1000), (4, 6, U), (16, 6, U), (32, 0, U)])
self.assertEqual(sa.read_only_acl(raw, 0o660, 1000), raw)
def test_existing_user_write_acl_requires_review(self):
for permission in (0, 2, 6, 7):
raw = acl([(1, 6, U), (2, permission, 1000), (4, 6, U), (16, 6, U), (32, 0, U)])
with self.assertRaisesRegex(RuntimeError, "EXISTING_USER_ACL"):
sa.read_only_acl(raw, 0o660, 1000)
def test_corrupt_acl_duplicate_user_and_missing_mask_rejected(self):
for raw in (b"invalid", acl([(1, 6, U), (4, 6, U), (32, 0, U), (2, 4, 1000)]),
acl([(1, 6, U), (2, 4, 1000), (2, 4, 1000), (4, 6, U), (16, 6, U), (32, 0, U)])):
with self.assertRaises(RuntimeError):
sa.read_only_acl(raw, 0o660, 1000)
class ProtectedFilesystemTests(unittest.TestCase):
def test_only_root_owned_regular_nonwritable_files_accepted(self):
sa.protected_stat(SimpleNamespace(st_mode=stat.S_IFREG | 0o644, st_uid=0))
for kind, perm, owner in ((stat.S_IFLNK, 0o777, 0), (stat.S_IFREG, 0o664, 0),
(stat.S_IFREG, 0o644, 1000), (stat.S_IFDIR, 0o755, 0)):
with self.assertRaises(RuntimeError):
sa.protected_stat(SimpleNamespace(st_mode=kind | perm, st_uid=owner))
def test_corrupt_backup_identity_is_refused_without_silently_revoking_record(self):
record = {"schema": 1, "uid": 1000, "meta": META, "before_acl": None,
"expected_acl": sa.encode(sa.read_only_acl(None, 0o660, 1000))}
with mock.patch.object(sa, "read_json", return_value=record):
self.assertEqual(sa.load_record(), record)
for changed in ({k: v for k, v in META.items() if k != "inode"},
dict(META, rdev=sa.os.makedev(29, 1)), dict(META, mode=True)):
with mock.patch.object(sa, "read_json", return_value=dict(record, meta=changed)):
with self.assertRaisesRegex(RuntimeError, "BACKUP_IDENTITY_INVALID"):
sa.load_record()
def test_private_backup_rejects_world_readability(self):
with self.assertRaisesRegex(RuntimeError, "PRIVATE_PATH_MODE"):
sa.protected_stat(SimpleNamespace(st_mode=stat.S_IFREG | 0o644, st_uid=0), private=True)
def test_secure_directory_rejects_symlink_ancestor(self):
root = SimpleNamespace(st_mode=stat.S_IFDIR | 0o755, st_uid=0)
link = SimpleNamespace(st_mode=stat.S_IFLNK | 0o777, st_uid=0)
with mock.patch.object(Path, "lstat", side_effect=[root, link]):
with self.assertRaisesRegex(RuntimeError, "PROTECTED_PATH_TYPE"):
sa.secure_directory(Path("/etc/framebridge-expanded"))
def test_nonroot_enable_disable_apply_never_open_device(self):
with mock.patch.object(sa.os, "geteuid", return_value=1000), \
mock.patch.object(sa, "account", return_value={"user": "fbtest", "uid": 1000}), \
mock.patch.object(sa.os, "open") as opened:
for action in (lambda: sa.enable("fbtest"), sa.disable, sa.apply_device):
with self.assertRaisesRegex(RuntimeError, "ROOT_REQUIRED"):
action()
opened.assert_not_called()
def test_root_target_account_is_rejected(self):
with mock.patch.object(sa.pwd, "getpwnam", return_value=SimpleNamespace(pw_uid=0, pw_name="root")):
with self.assertRaisesRegex(RuntimeError, "ROOT_ACCOUNT_NOT_ALLOWED"):
sa.account("root")
def test_policy_remains_revocable_after_account_is_removed(self):
with mock.patch.object(sa.pwd, "getpwnam", side_effect=KeyError("deleted")) as lookup:
self.assertEqual(sa.validate_policy(POLICY), POLICY)
lookup.assert_not_called()
with mock.patch.object(sa.pwd, "getpwnam", side_effect=KeyError("deleted")):
with self.assertRaisesRegex(RuntimeError, "ACCOUNT_NOT_FOUND"):
sa.apply_locked(POLICY)
def test_uid_reuse_name_change_is_rejected(self):
with mock.patch.object(sa.pwd, "getpwnam", return_value=SimpleNamespace(pw_uid=1002, pw_name="fbtest")):
with self.assertRaisesRegex(RuntimeError, "ACCOUNT_IDENTITY_CHANGED"):
sa.account("fbtest", 1000)
class RestoreTests(unittest.TestCase):
def record(self, before=None):
return {"schema": 1, "uid": 1000, "meta": META,
"before_acl": sa.encode(before), "expected_acl": sa.encode(sa.read_only_acl(before, 0o660, 1000))}
def test_revoke_plain_acl_restores_absent_xattr(self):
record = self.record()
with mock.patch.object(sa, "device_snapshot", side_effect=[(META, sa.decode(record["expected_acl"])), (META, None)]), \
mock.patch.object(sa.os, "removexattr") as removed, mock.patch.object(sa.os, "setxattr") as written:
self.assertEqual(sa.restore_owned(9, record), "ORIGINAL_ACL_RESTORED")
removed.assert_called_once_with(9, sa.XATTR)
written.assert_not_called()
def test_revoke_restores_exact_original_extended_acl(self):
before = acl(EXTENDED)
record = self.record(before)
with mock.patch.object(sa, "device_snapshot", side_effect=[(META, sa.decode(record["expected_acl"])), (META, before)]), \
mock.patch.object(sa.os, "setxattr") as written:
sa.restore_owned(9, record)
written.assert_called_once_with(9, sa.XATTR, before, sa.os.XATTR_REPLACE)
def test_external_acl_change_is_not_overwritten(self):
with mock.patch.object(sa, "device_snapshot", return_value=(META, b"external")), \
mock.patch.object(sa.os, "removexattr") as removed, mock.patch.object(sa.os, "setxattr") as written:
with self.assertRaisesRegex(RuntimeError, "ACL_CHANGED_REVIEW"):
sa.restore_owned(9, self.record())
removed.assert_not_called()
written.assert_not_called()
def test_chmod_chgrp_or_mask_change_of_same_device_requires_review(self):
for changed in (dict(META, gid=55), dict(META, mode=0o640), dict(META, uid=1)):
with mock.patch.object(sa, "device_snapshot", return_value=(changed, sa.decode(self.record()["expected_acl"]))), \
mock.patch.object(sa.os, "removexattr") as removed, mock.patch.object(sa.os, "setxattr") as written:
with self.assertRaisesRegex(RuntimeError, "METADATA_CHANGED_REVIEW"):
sa.restore_owned(9, self.record())
removed.assert_not_called()
written.assert_not_called()
def test_recreated_device_is_not_touched_by_old_backup(self):
with mock.patch.object(sa, "device_snapshot", return_value=(dict(META, boot="new-boot"), None)), \
mock.patch.object(sa.os, "removexattr") as removed:
self.assertEqual(sa.restore_owned(9, self.record()), "DEVICE_RECREATED_NO_OLD_GRANT")
removed.assert_not_called()
def test_crash_before_acl_write_and_repeated_restore_are_idempotent(self):
with mock.patch.object(sa, "device_snapshot", return_value=(META, None)), \
mock.patch.object(sa.os, "removexattr") as removed:
self.assertEqual(sa.restore_owned(9, self.record()), "ORIGINAL_ACL_ALREADY_RESTORED")
removed.assert_not_called()
class TransactionTests(unittest.TestCase):
@contextlib.contextmanager
def simulated(self, *, initial=None, record=None):
state = {"acl": initial, "record": record, "writes": []}
def save(path, value, **kwargs):
state["writes"].append((path.name, value))
if path.name == "active.json":
state["record"] = value
def set_acl(fd, name, value, flags):
state["acl"] = value
def remove_acl(fd, name):
state["acl"] = None
with ExitStack() as stack:
for name, value in [
("account", mock.Mock(return_value={"user": "fbtest", "uid": 1000})),
("device_snapshot", mock.Mock(side_effect=lambda fd: (META, state["acl"]))),
("load_record", mock.Mock(side_effect=lambda: state["record"])),
("write_json", mock.Mock(side_effect=save)),
("probe_user", mock.Mock()),
]:
state[name] = stack.enter_context(mock.patch.object(sa, name, value))
state["opened"] = stack.enter_context(mock.patch.object(sa.os, "open", return_value=9))
stack.enter_context(mock.patch.object(sa.os, "close"))
state["set"] = stack.enter_context(mock.patch.object(sa.os, "setxattr", side_effect=set_acl))
state["remove"] = stack.enter_context(mock.patch.object(sa.os, "removexattr", side_effect=remove_acl))
yield state
def test_apply_backs_up_before_write_and_remains_read_only(self):
with self.simulated() as state:
answer = sa.apply_locked(POLICY)
self.assertEqual(answer["status"], "ENABLED_READ_ONLY")
self.assertEqual(state["writes"][0][0][:7], "backup-")
self.assertEqual(state["writes"][1][0], "active.json")
self.assertIn((2, 4, 1000), sa.acl_entries(state["acl"], 0o660))
self.assertEqual(state["set"].call_args.args[-1], sa.os.XATTR_CREATE)
def test_second_apply_is_idempotent_and_does_not_rebackup(self):
with self.simulated() as state:
sa.apply_locked(POLICY)
state["set"].reset_mock()
state["writes"].clear()
self.assertFalse(sa.apply_locked(POLICY)["changed"])
state["set"].assert_not_called()
self.assertEqual(state["writes"], [])
def test_reapply_never_adopts_changed_metadata_as_fresh_baseline(self):
with self.simulated() as state:
sa.apply_locked(POLICY)
state["set"].reset_mock()
state["writes"].clear()
state["device_snapshot"].side_effect = lambda fd: (dict(META, mode=0o640), state["acl"])
with self.assertRaisesRegex(RuntimeError, "METADATA_CHANGED_REVIEW"):
sa.apply_locked(POLICY)
state["set"].assert_not_called()
self.assertEqual(state["writes"], [])
def test_probe_failure_rolls_back_exact_original_acl(self):
with self.simulated() as state:
state["probe_user"].side_effect = RuntimeError("PROBE_TEST_FAILURE")
with self.assertRaisesRegex(RuntimeError, "PROBE_TEST_FAILURE"):
sa.apply_locked(POLICY)
self.assertIsNone(state["acl"])
state["remove"].assert_called_once()
self.assertEqual(sa.restore_owned(9, state["record"]), "ORIGINAL_ACL_ALREADY_RESTORED")
def test_external_acl_change_blocks_reapply(self):
with self.simulated() as state:
sa.apply_locked(POLICY)
state["acl"] = b"external"
state["set"].reset_mock()
with self.assertRaisesRegex(RuntimeError, "ACL_CHANGED_REVIEW"):
sa.apply_locked(POLICY)
state["set"].assert_not_called()
def test_disabled_udev_invocation_does_not_open_device(self):
with mock.patch.object(sa, "locked", contextlib.nullcontext), \
mock.patch.object(sa, "load_policy", return_value=dict(POLICY, enabled=False)), \
mock.patch.object(sa, "apply_locked") as apply:
self.assertEqual(sa.apply_device()["status"], "DISABLED")
apply.assert_not_called()
def test_disable_without_policy_is_successful_idempotent(self):
with mock.patch.object(sa, "locked", contextlib.nullcontext), \
mock.patch.object(sa, "load_policy", return_value=None), \
mock.patch.object(sa, "reload_rules") as reload, mock.patch.object(sa.os, "open") as opened:
self.assertFalse(sa.disable()["changed"])
reload.assert_not_called()
opened.assert_not_called()
def test_disable_conflict_deactivates_policy_preserves_backup_and_tombstone(self):
record = {"schema": 1, "uid": 1000, "meta": META, "before_acl": None, "expected_acl": "AA=="}
with mock.patch.object(sa, "locked", contextlib.nullcontext), \
mock.patch.object(sa, "load_policy", return_value=POLICY), \
mock.patch.object(sa, "reload_rules"), mock.patch.object(sa, "load_record", return_value=record), \
mock.patch.object(sa, "write_json") as written, \
mock.patch.object(sa.os, "open", return_value=9), mock.patch.object(sa.os, "close"), \
mock.patch.object(sa, "restore_owned", side_effect=RuntimeError("FRAMEBUFFER_ACL_CHANGED_REVIEW_REQUIRED")), \
mock.patch.object(Path, "unlink") as removed:
with self.assertRaisesRegex(RuntimeError, "ACL_CHANGED_REVIEW"):
sa.disable()
self.assertFalse(written.call_args.args[1]["enabled"])
removed.assert_not_called()
class ProbeAndStatusTests(unittest.TestCase):
def test_read_probe_drops_all_privileges_without_pam_or_shell(self):
user = SimpleNamespace(pw_uid=1000, pw_gid=1000, pw_name="fbtest")
with mock.patch.object(sa.pwd, "getpwnam", return_value=user), \
mock.patch.object(sa.os, "getgrouplist", return_value=[1000, 27]), \
mock.patch.object(sa.subprocess, "run", return_value=SimpleNamespace(returncode=0, stdout=b'{"read_open":true,"write_access":false}')) as run:
sa.probe_user(POLICY)
self.assertEqual(run.call_args.kwargs["user"], 1000)
self.assertEqual(run.call_args.kwargs["group"], 1000)
self.assertEqual(run.call_args.kwargs["extra_groups"], [1000, 27])
self.assertEqual(run.call_args.args[0][:3], ["/usr/bin/python3", "-I", "-S"])
self.assertNotIn("shell", run.call_args.kwargs)
def test_probe_rejects_write_permission(self):
user = SimpleNamespace(pw_uid=1000, pw_gid=1000, pw_name="fbtest")
with mock.patch.object(sa.pwd, "getpwnam", return_value=user), \
mock.patch.object(sa.os, "getgrouplist", return_value=[1000]), \
mock.patch.object(sa.subprocess, "run", return_value=SimpleNamespace(returncode=0, stdout=b'{"read_open":true,"write_access":true}')):
with self.assertRaisesRegex(RuntimeError, "ACCESS_NOT_READ_ONLY"):
sa.probe_user(POLICY)
def test_status_never_locks_writes_or_changes_permissions(self):
with mock.patch.object(sa, "load_policy", return_value=POLICY), \
mock.patch.object(sa, "locked") as lock, \
mock.patch.object(sa, "write_json") as write, \
mock.patch.object(sa.os, "setxattr") as aclwrite:
result = sa.status()
self.assertTrue(result["enabled"])
lock.assert_not_called()
write.assert_not_called()
aclwrite.assert_not_called()
def test_invalid_device_is_refused_before_reading_acl(self):
for mode, dev in ((stat.S_IFREG | 0o660, 0), (stat.S_IFCHR | 0o660, sa.os.makedev(29, 1))):
info = SimpleNamespace(st_mode=mode, st_rdev=dev, st_uid=0)
with mock.patch.object(sa.os, "fstat", return_value=info), \
mock.patch.object(sa.os, "getxattr") as get:
with self.assertRaisesRegex(RuntimeError, "FRAMEBUFFER_DEVICE_INVALID"):
sa.device_snapshot(9)
get.assert_not_called()
class CliAndRuleTests(unittest.TestCase):
def test_status_dispatch_ignores_user_profile_state(self):
with mock.patch.object(cli, "resolve_state", side_effect=AssertionError("must not resolve profile")), \
mock.patch.object(cli, "_module", return_value=SimpleNamespace(command=lambda args: {"enabled": False})), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(cli.main(["screen-access", "status"]), 0)
def test_enable_requires_explicit_account_and_parser_has_no_owner_or_pam_flow(self):
args = cli.build_parser().parse_args(["screen-access", "enable", "--user", "fbtest"])
self.assertEqual((args.screen_access_command, args.user), ("enable", "fbtest"))
with contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit):
cli.build_parser().parse_args(["screen-access", "enable"])
def test_rule_matches_only_fb0_and_is_inactive_without_policy(self):
source = Path(__file__).resolve().parents[1]
text = (source / "packaging/70-framebridge-screen.rules").read_text()
lines = [line for line in text.splitlines() if line and not line.startswith("#")]
self.assertEqual(len(lines), 2)
for line in lines:
self.assertIn('SUBSYSTEM=="graphics"', line)
self.assertIn('KERNEL=="fb0"', line)
self.assertIn('TEST=="/etc/framebridge-expanded/screen-access.json"', line)
self.assertIn("framebridge-screen-access apply-device", line)
self.assertNotIn("uaccess", text)
self.assertNotIn("video", text)
if __name__ == "__main__":
unittest.main()
+475
View File
@@ -0,0 +1,475 @@
"""Real-pixel capture and immutable-artifact regression tests (no display needed)."""
import base64
from dataclasses import replace
import hashlib
import json
import os
from pathlib import Path
import struct
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import MagicMock, Mock, patch
import zlib
from framebridge_expanded.real_screen import screen_capture as screen
def geometry(**changes):
base = screen.Geometry(width=2, height=1, virtual_width=2, virtual_height=1,
xoffset=0, yoffset=0, stride=8, bpp=32,
red=(16, 8), green=(8, 8), blue=(0, 8),
memory_bytes=8, driver="unit-test", byteorder="little")
return replace(base, **changes)
def png_pixels(data):
"""Parse PNG framing/CRC and decode exact RGB or packed palette pixel rows."""
assert data[:8] == b"\x89PNG\r\n\x1a\n"
offset = 8
rows = b""
size = None
palette = None
depth = color = None
seen_end = False
while offset < len(data):
length = struct.unpack(">I", data[offset:offset + 4])[0]
kind = data[offset + 4:offset + 8]
body = data[offset + 8:offset + 8 + length]
crc = struct.unpack(">I", data[offset + 8 + length:offset + 12 + length])[0]
assert crc == zlib.crc32(kind + body) & 0xffffffff
if kind == b"IHDR":
w, h, depth, color, compression, filtering, interlace = struct.unpack(">IIBBBBB", body)
assert (compression, filtering, interlace) == (0, 0, 0)
assert (color == 2 and depth == 8) or (color == 3 and depth in (1, 2, 4, 8))
size = (w, h)
elif kind == b"PLTE":
assert len(body) > 0 and len(body) % 3 == 0
palette = [body[i:i + 3] for i in range(0, len(body), 3)]
elif kind == b"IDAT":
rows += body
elif kind == b"IEND":
assert body == b""
seen_end = True
offset += length + 12
assert offset == len(data) and size and seen_end
scan = zlib.decompress(rows)
width, height = size
row_bytes = width * 3 if color == 2 else (width * depth + 7) // 8
assert len(scan) == height * (1 + row_bytes)
pixels = bytearray()
for y in range(height):
start = y * (1 + row_bytes)
assert scan[start] == 0
row = scan[start + 1:start + 1 + row_bytes]
if color == 2:
pixels.extend(row)
else:
assert palette is not None and len(palette) <= 1 << depth
for x in range(width):
shift = 8 - depth * (1 + x % (8 // depth))
index = (row[x // (8 // depth)] >> shift) & ((1 << depth) - 1)
assert index < len(palette)
pixels.extend(palette[index])
return size, bytes(pixels)
class CaptureTests(unittest.TestCase):
def assert_code(self, code, function, *args, **kwargs):
with self.assertRaises(screen.CaptureError) as caught:
function(*args, **kwargs)
self.assertEqual(caught.exception.code, code)
def test_real_bgra_viewport_ignores_padding_and_hidden_pixels(self):
g = geometry(width=2, height=2, virtual_width=4, virtual_height=4,
xoffset=1, yoffset=1, stride=20, memory_bytes=80)
raw = bytearray(b"\xa5" * g.memory_bytes)
expected = bytearray()
for y in range(4):
for x in range(4):
r, green, b = 10 * y + x, 20 * y + x, 30 * y + x
start = y * g.stride + x * 4
raw[start:start + 4] = bytes((b, green, r, 255))
if 1 <= y <= 2 and 1 <= x <= 2:
expected.extend((r, green, b))
self.assertEqual(screen.decode_framebuffer(bytes(raw), g), bytes(expected))
def test_real_big_endian_rgb24_with_row_padding(self):
g = geometry(width=2, height=2, virtual_width=2, virtual_height=2,
bpp=24, stride=8, memory_bytes=16, byteorder="big")
raw = bytes((1, 2, 3, 4, 5, 6, 0xa5, 0xa5, 7, 8, 9, 10, 11, 12, 0xa5, 0xa5))
self.assertEqual(screen.decode_framebuffer(raw, g), bytes(range(1, 13)))
def test_rgb565_channel_scaling(self):
g = geometry(width=4, virtual_width=4, bpp=16, stride=8, memory_bytes=8,
red=(11, 5), green=(5, 6), blue=(0, 5))
raw = struct.pack("<4H", 0xf800, 0x07e0, 0x001f, 0xffff)
self.assertEqual(screen.decode_framebuffer(raw, g),
bytes((255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255)))
def test_partial_framebuffer_is_rejected(self):
self.assert_code("FRAMEBUFFER_READ_SHORT", screen.decode_framebuffer, b"\0" * 7, geometry())
def test_overlapping_rgb_fields_are_rejected(self):
self.assert_code("COLOR_LAYOUT_OVERLAP", screen.validate_geometry,
geometry(green=(16, 8)))
def test_ten_bit_channels_are_rejected_instead_of_silently_quantized(self):
self.assert_code("COLOR_LAYOUT_UNSUPPORTED", screen.decode_framebuffer, b"\xff" * 8,
geometry(red=(20, 10), green=(10, 10), blue=(0, 10)))
def test_viewport_outside_virtual_screen_is_rejected(self):
self.assert_code("VIEWPORT_INVALID", screen.validate_geometry,
geometry(xoffset=1))
def test_stride_cannot_hide_missing_pixels(self):
self.assert_code("FRAMEBUFFER_LAYOUT_INVALID", screen.validate_geometry,
geometry(stride=4))
def test_png_preserves_each_pixel_without_overlays(self):
pixels = bytes((1, 2, 3, 255, 0, 0, 0, 255, 0, 0, 0, 255))
self.assertEqual(png_pixels(screen.encode_png(pixels, 2, 2)), ((2, 2), pixels))
def test_png_supports_more_than_256_distinct_colors(self):
pixels = b"".join(bytes((i % 256, i // 256, i % 113)) for i in range(1024))
self.assertGreater(len({pixels[i:i + 3] for i in range(0, len(pixels), 3)}), 256)
self.assertEqual(png_pixels(screen.encode_png(pixels, 32, 32)), ((32, 32), pixels))
def assert_indexed_pixels(self, color_count, expected_depth):
# Odd row widths exercise unused packed bits and independent row starts.
width = color_count + (2 if color_count % 2 else 1)
height = 2
colors = [bytes((i, 255 - i, i * 37 % 256)) for i in range(color_count)]
pixels = b"".join(colors[i % color_count] for i in range(width * height))
png = screen.encode_png(pixels, width, height, prefer_indexed=True)
self.assertEqual((png[24], png[25]), (expected_depth, 3))
self.assertEqual(png_pixels(png), ((width, height), pixels))
def test_palette_one_bit_storage_preserves_one_and_two_colors(self):
for count in (1, 2):
with self.subTest(count=count):
self.assert_indexed_pixels(count, 1)
def test_palette_two_bit_storage_preserves_three_and_four_colors(self):
for count in (3, 4):
with self.subTest(count=count):
self.assert_indexed_pixels(count, 2)
def test_palette_four_bit_storage_preserves_five_and_sixteen_colors(self):
for count in (5, 16):
with self.subTest(count=count):
self.assert_indexed_pixels(count, 4)
def test_palette_eight_bit_storage_preserves_seventeen_and_256_colors(self):
for count in (17, 256):
with self.subTest(count=count):
self.assert_indexed_pixels(count, 8)
def test_palette_attempt_at_257_colors_falls_back_without_pixel_loss(self):
pixels = b"".join(bytes((i % 256, i // 256, i * 37 % 256)) for i in range(257))
self.assertEqual(len({pixels[i:i + 3] for i in range(0, len(pixels), 3)}), 257)
png = screen.encode_png(pixels, 257, 1, prefer_indexed=True)
self.assertEqual((png[24], png[25]), (8, 2))
self.assertEqual(png_pixels(png), ((257, 1), pixels))
def test_default_rgb_format_stays_truecolor_even_for_palette_sized_input(self):
pixels = bytes((1, 2, 3, 4, 5, 6))
png = screen.encode_png(pixels, 2, 1)
self.assertEqual((png[24], png[25]), (8, 2))
self.assertEqual(png_pixels(png), ((2, 1), pixels))
def tty_session(self, **changes):
s = {"Id": "2", "User": "1000", "Leader": "1234", "Active": "yes", "Remote": "no",
"Type": "tty", "Seat": "seat0", "VTNr": "1", "TTY": "tty1"}
s.update(changes)
return s
def test_foreground_identifies_actual_active_vt(self):
s = self.tty_session()
selected = screen.choose_foreground("tty1", [s, self.tty_session(Id="3", Active="no")], "seat0")
self.assertEqual((selected.active_vt, selected.session_id, selected.leader), ("tty1", "2", 1234))
def test_active_graphical_session_never_uses_framebuffer_fallback(self):
for mode in ("x11", "wayland"):
with self.subTest(mode=mode):
self.assert_code("GRAPHICAL_CAPTURE_REQUIRED", screen.choose_foreground,
"tty1", [self.tty_session(Type=mode)], "seat0")
def test_unmatched_vt_is_rejected(self):
self.assert_code("FOREGROUND_SESSION_UNVERIFIED", screen.choose_foreground,
"tty2", [self.tty_session()], "seat0")
def test_remote_session_does_not_bind_local_display(self):
self.assert_code("FOREGROUND_SESSION_UNVERIFIED", screen.choose_foreground,
"tty1", [self.tty_session(Remote="yes")], "seat0")
def test_foreground_switch_during_capture_is_rejected(self):
before = screen.choose_foreground("tty1", [self.tty_session()], "seat0")
screen.require_unchanged(before, before)
self.assert_code("FOREGROUND_CHANGED_DURING_CAPTURE", screen.require_unchanged,
before, replace(before, session_id="4"))
def x11_session(self, **changes):
s = self.tty_session(Type="x11", User=str(os.getuid()), TTY="tty7", VTNr="7", Display=":0")
s.update(changes)
return s
def test_display_selector_accepts_same_user_active_local_x11_vt(self):
f = screen.choose_display_foreground("tty7", [self.x11_session()], "seat0")
self.assertEqual((f.session_type, f.uid, f.display, f.vtnr), ("x11", os.getuid(), ":0", 7))
def test_lightdm_empty_tty_keeps_verified_numeric_vt(self):
f = screen.choose_display_foreground("tty7", [self.x11_session(TTY="")], "seat0")
self.assertEqual((f.active_vt, f.tty, f.vtnr, f.display), ("tty7", "", 7, ":0"))
def test_lightdm_empty_tty_never_accepts_wrong_vt_user_or_remote_session(self):
cases = [({"VTNr": "8"}, "GRAPHICAL_VT_UNVERIFIED"),
({"User": str(os.getuid() + 1)}, "X11_SESSION_IDENTITY_MISMATCH"),
({"Remote": "yes"}, "FOREGROUND_SESSION_UNVERIFIED")]
for changes, code in cases:
with self.subTest(changes=changes):
s = self.x11_session(TTY="", **changes)
self.assert_code(code, screen.choose_display_foreground, "tty7", [s], "seat0")
def test_lightdm_explicit_contradictory_tty_is_rejected(self):
self.assert_code("GRAPHICAL_VT_UNVERIFIED", screen.choose_display_foreground,
"tty7", [self.x11_session(TTY="tty8")], "seat0")
def test_lightdm_missing_tty_metadata_is_not_treated_as_empty(self):
s = self.x11_session()
del s["TTY"]
self.assert_code("GRAPHICAL_VT_UNVERIFIED", screen.choose_display_foreground,
"tty7", [s], "seat0")
def test_lightdm_capture_identity_rejects_vt_switch_or_session_replacement(self):
before = screen.choose_display_foreground("tty7", [self.x11_session(TTY="")], "seat0")
for after in [replace(before, active_vt="tty8", vtnr=8),
replace(before, session_id="replacement"),
replace(before, display=":1"), replace(before, uid=before.uid + 1)]:
with self.subTest(after=after):
self.assert_code("FOREGROUND_CHANGED_DURING_CAPTURE",
screen.require_unchanged, before, after)
def test_display_selector_keeps_text_vt_identity(self):
f = screen.choose_display_foreground("tty1", [self.tty_session()], "seat0")
self.assertEqual((f.session_type, f.display, f.active_vt), ("tty", "", "tty1"))
def test_display_selector_refuses_wayland_instead_of_stale_fbdev(self):
self.assert_code("WAYLAND_CAPTURE_UNSUPPORTED", screen.choose_display_foreground,
"tty7", [self.x11_session(Type="wayland")], "seat0")
def test_display_selector_refuses_network_and_invalid_display_names(self):
for display in ("host:0", "localhost:0", "tcp/localhost:0", ":0;anything", ""):
with self.subTest(display=display):
self.assert_code("X11_DISPLAY_UNVERIFIED", screen.choose_display_foreground,
"tty7", [self.x11_session(Display=display)], "seat0")
def test_display_selector_requires_matching_vt_and_identity(self):
self.assert_code("GRAPHICAL_VT_UNVERIFIED", screen.choose_display_foreground,
"tty8", [self.x11_session()], "seat0")
self.assert_code("X11_SESSION_IDENTITY_MISMATCH", screen.choose_display_foreground,
"tty7", [self.x11_session(User=str(os.getuid() + 1))], "seat0")
def test_display_selector_requires_one_active_seat_session(self):
self.assert_code("FOREGROUND_SESSION_UNVERIFIED", screen.choose_display_foreground,
"tty7", [self.x11_session(), self.x11_session(Id="3")], "seat0")
def test_x11_color_masks_accept_contiguous_fields_only(self):
self.assertEqual(screen._mask_field(0xff0000), (16, 8))
self.assertEqual(screen._mask_field(0x07e0), (5, 6))
self.assertEqual(screen._mask_field(1), (0, 1))
for mask in (0, -1, 0b101, 0xff00ff):
with self.subTest(mask=mask):
self.assert_code("X11_COLOR_MASK_INVALID", screen._mask_field, mask)
def test_root_display_manager_environment_is_never_read(self):
f = screen.Foreground("tty7", "19", 1000, 3668, "x11", "", "seat0", 7, ":0")
leader = MagicMock()
leader.stat.return_value = SimpleNamespace(st_uid=0)
with patch.object(screen, "Path") as path:
path.return_value.__truediv__.return_value = leader
self.assertEqual(screen._x11_leader_environment(f, ":0"), {})
leader.__truediv__.assert_not_called()
def test_foreign_user_session_leader_is_rejected(self):
f = screen.Foreground("tty7", "19", 1000, 3668, "x11", "", "seat0", 7, ":0")
leader = MagicMock()
leader.stat.return_value = SimpleNamespace(st_uid=1001)
with patch.object(screen, "Path") as path:
path.return_value.__truediv__.return_value = leader
self.assert_code("X11_LEADER_IDENTITY_MISMATCH",
screen._x11_leader_environment, f, ":0")
leader.__truediv__.assert_not_called()
def test_user_leader_context_rejects_a_different_display(self):
f = screen.Foreground("tty7", "19", os.getuid(), 3668, "x11", "", "seat0", 7, ":0")
leader = MagicMock()
leader.stat.return_value = SimpleNamespace(st_uid=f.uid)
leader.__truediv__ = Mock(return_value=SimpleNamespace(read_bytes=lambda: b"DISPLAY=:1\0"))
with patch.object(screen, "Path") as path:
path.return_value.__truediv__.return_value = leader
self.assert_code("X11_DISPLAY_IDENTITY_MISMATCH",
screen._x11_leader_environment, f, ":0")
def private_home_context(self, home, **changes):
f = screen.choose_display_foreground("tty7", [self.x11_session(TTY="")], "seat0")
f = replace(f, **changes)
account = SimpleNamespace(pw_uid=f.uid, pw_dir=str(home))
return f, account
def test_lightdm_root_leader_falls_back_to_validated_same_user_authority(self):
with tempfile.TemporaryDirectory() as home:
authority = Path(home) / ".Xauthority"
authority.touch(mode=0o600)
f, account = self.private_home_context(home)
with patch.dict(os.environ, {}, clear=True), \
patch.object(screen, "_x11_leader_environment", return_value={}), \
patch.object(screen.pwd, "getpwuid", return_value=account):
self.assertEqual(screen._x11_context(f, None, None), (":0", str(authority)))
def test_canonical_home_fallback_ignores_callers_home(self):
with tempfile.TemporaryDirectory() as home:
authority = Path(home) / ".Xauthority"
authority.touch(mode=0o600)
f, account = self.private_home_context(home)
with patch.dict(os.environ, {"HOME": "/unrelated-user"}, clear=True), \
patch.object(screen, "_x11_leader_environment", return_value={}), \
patch.object(screen.pwd, "getpwuid", return_value=account):
self.assertEqual(screen._x11_context(f, None, None)[1], str(authority))
def test_authority_fallback_rejects_missing_world_readable_or_symlink_file(self):
with tempfile.TemporaryDirectory() as home:
authority = Path(home) / ".Xauthority"
target = Path(home) / "target"
f, account = self.private_home_context(home)
with patch.dict(os.environ, {}, clear=True), \
patch.object(screen, "_x11_leader_environment", return_value={}), \
patch.object(screen.pwd, "getpwuid", return_value=account):
self.assert_code("X11_AUTHORITY_UNAVAILABLE", screen._x11_context, f, None, None)
authority.touch(mode=0o644)
# Ensure the negative fixture stays world-readable under a private umask.
authority.chmod(0o644)
self.assert_code("X11_AUTHORITY_UNTRUSTED", screen._x11_context, f, None, None)
authority.unlink()
target.touch(mode=0o600)
authority.symlink_to(target)
self.assert_code("X11_AUTHORITY_UNTRUSTED", screen._x11_context, f, None, None)
def test_authority_fallback_rejects_foreign_file_owner(self):
with tempfile.TemporaryDirectory() as home:
authority = Path(home) / ".Xauthority"
authority.touch(mode=0o600)
f, account = self.private_home_context(home, uid=os.getuid() + 1)
with patch.dict(os.environ, {}, clear=True), \
patch.object(screen.os, "getuid", return_value=f.uid), \
patch.object(screen, "_x11_leader_environment", return_value={}), \
patch.object(screen.pwd, "getpwuid", return_value=account):
self.assert_code("X11_AUTHORITY_UNTRUSTED", screen._x11_context, f, None, None)
def test_authority_fallback_rejects_missing_account_or_relative_home(self):
f, account = self.private_home_context("relative-home")
with patch.dict(os.environ, {}, clear=True), \
patch.object(screen, "_x11_leader_environment", return_value={}):
with patch.object(screen.pwd, "getpwuid", side_effect=KeyError):
self.assert_code("X11_AUTHORITY_UNAVAILABLE", screen._x11_context, f, None, None)
with patch.object(screen.pwd, "getpwuid", return_value=account):
self.assert_code("X11_AUTHORITY_PATH_INVALID", screen._x11_context, f, None, None)
def test_context_rejects_explicit_display_or_user_mismatch(self):
f, _ = self.private_home_context("/unused")
self.assert_code("X11_DISPLAY_IDENTITY_MISMATCH", screen._x11_context, f, ":1", None)
self.assert_code("X11_SESSION_IDENTITY_MISMATCH", screen._x11_context,
replace(f, uid=f.uid + 1), None, None)
def artifact(self, directory, data=b"0123456789"):
cid = "1" * 32
digest = hashlib.sha256(data).hexdigest()
manifest = {"capture_id": cid, "sha256": digest, "bytes": len(data),
"synthetic": False, "pixel_source_verified": True}
screen._write_new(Path(directory) / (cid + ".png"), data)
screen._write_new(Path(directory) / (cid + ".json"), json.dumps(manifest).encode())
return cid, digest, manifest
def test_artifact_write_cannot_overwrite_existing_file(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "one.png"
screen._write_new(path, b"first")
self.assert_code("CAPTURE_ARTIFACT_CREATE_FAILED", screen._write_new, path, b"second")
self.assertEqual(path.read_bytes(), b"first")
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
def test_chunks_reassemble_exact_immutable_bytes_and_digest(self):
with tempfile.TemporaryDirectory() as directory:
cid, digest, _ = self.artifact(directory)
results = [screen.read_chunk(directory, cid, i, chunk_bytes=4, expected_sha256=digest)
for i in range(3)]
self.assertEqual(b"".join(base64.b64decode(p["data"], validate=True) for p in results),
b"0123456789")
self.assertEqual([p["chunk_bytes"] for p in results], [4, 4, 2])
self.assertEqual([p["more"] for p in results], [True, True, False])
self.assertTrue(all(p["sha256"] == digest and p["total_chunks"] == 3 for p in results))
def test_chunk_index_and_boolean_are_rejected(self):
with tempfile.TemporaryDirectory() as directory:
cid, _, _ = self.artifact(directory)
self.assert_code("CHUNK_INDEX_OUT_OF_RANGE", screen.read_chunk, directory, cid, 1)
self.assert_code("CHUNK_BOUNDS_INVALID", screen.read_chunk, directory, cid, True)
self.assert_code("CHUNK_BOUNDS_INVALID", screen.read_chunk, directory, cid, 0,
chunk_bytes=screen.MAX_CHUNK_BYTES + 1)
def test_capture_id_cannot_escape_artifact_directory(self):
with tempfile.TemporaryDirectory() as directory:
self.assert_code("CAPTURE_ID_INVALID", screen.read_chunk, directory, "../target", 0)
def test_mutated_image_bytes_are_rejected(self):
with tempfile.TemporaryDirectory() as directory:
cid, _, _ = self.artifact(directory)
(Path(directory) / (cid + ".png")).write_bytes(b"changed")
self.assert_code("CAPTURE_SHA256_MISMATCH", screen.read_chunk, directory, cid, 0)
def test_expected_digest_is_enforced(self):
with tempfile.TemporaryDirectory() as directory:
cid, _, _ = self.artifact(directory)
self.assert_code("EXPECTED_SHA256_MISMATCH", screen.read_chunk, directory, cid, 0,
expected_sha256="0" * 64)
def test_image_symlink_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
cid, _, _ = self.artifact(directory)
p = Path(directory) / (cid + ".png")
target = Path(directory) / "target.png"
p.rename(target)
p.symlink_to(target)
self.assert_code("CAPTURE_ARTIFACT_UNAVAILABLE", screen.read_chunk, directory, cid, 0)
def test_hard_linked_image_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
cid, _, _ = self.artifact(directory)
os.link(Path(directory) / (cid + ".png"), Path(directory) / "alias.png")
self.assert_code("CAPTURE_ARTIFACT_UNTRUSTED", screen.read_chunk, directory, cid, 0)
def test_artifact_directory_cannot_be_a_symlink(self):
with tempfile.TemporaryDirectory() as directory:
target = Path(directory) / "real"
target.mkdir(mode=0o700)
link = Path(directory) / "linked"
link.symlink_to(target, target_is_directory=True)
self.assert_code("CAPTURE_DIRECTORY_UNTRUSTED", screen._directory, link)
def test_synthetic_manifest_is_not_accepted_as_real_image(self):
with tempfile.TemporaryDirectory() as directory:
cid, _, manifest = self.artifact(directory)
manifest["synthetic"] = True
(Path(directory) / (cid + ".json")).write_text(json.dumps(manifest))
self.assert_code("CAPTURE_MANIFEST_INVALID", screen.read_chunk, directory, cid, 0)
def test_manifest_requires_json_object_not_null_or_array(self):
for invalid in (None, [], ["capture_id"], "text", 3):
with self.subTest(invalid=invalid), tempfile.TemporaryDirectory() as directory:
cid, _, _ = self.artifact(directory)
(Path(directory) / (cid + ".json")).write_text(json.dumps(invalid))
self.assert_code("CAPTURE_MANIFEST_INVALID", screen.read_chunk, directory, cid, 0)
if __name__ == "__main__":
unittest.main()
+163
View File
@@ -0,0 +1,163 @@
import copy
from datetime import datetime, timezone, timedelta
import hashlib
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from framebridge_expanded.real_screen import screen_control as m
class ScreenControlTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.path = Path(self.temp.name) / "screen.png"
self.path.write_bytes(b"immutable-observed-frame-fixture")
self.binding = {"version": 1, "backend": "visible-tmux", "node": "TESTNODE",
"uid": os.getuid(), "active_vt": "tty1", "client_tty": "/dev/tty1",
"socket": {"path": "/run/user/1000/console.sock", "inode": 12},
"server": {"pid": 100, "start_ticks": 20},
"client": {"pid": 101, "start_ticks": 21},
"session_id": "$0", "window_id": "@0", "pane_id": "%1",
"pane": {"pid": 102, "start_ticks": 22},
"pane_geometry": [120, 0, 120, 66], "pane_in_mode": False}
self.manifest = {"capture_id": "a" * 32, "source": "linux-framebuffer",
"screen_type": "tty", "active_vt": "tty1", "node": "TESTNODE",
"uid": os.getuid(), "synthetic": False, "pixel_source_verified": True,
"path": str(self.path), "sha256": hashlib.sha256(self.path.read_bytes()).hexdigest(),
"completed_at": datetime.now(timezone.utc).isoformat(),
"binding": copy.deepcopy(self.binding)}
def tearDown(self):
self.temp.cleanup()
def refused(self, change, code="SCREEN_BINDING_CHANGED"):
different = copy.deepcopy(self.binding)
change(different)
with patch.object(m, "snapshot_binding", return_value=different), patch.object(m, "_tmux") as send:
with self.assertRaises(m.ControlError) as e:
m.act(self.manifest, "key.send", key="ENTER")
self.assertEqual(e.exception.code, code)
send.assert_not_called()
def test_bind_returns_copy(self):
with patch.object(m, "snapshot_binding", return_value=self.binding):
result = m.bind_screen(self.manifest)
self.assertIsNot(result, self.manifest)
self.assertEqual(result["binding"], self.binding)
def test_bind_vt_mismatch(self):
self.manifest["active_vt"] = "tty2"
with patch.object(m, "snapshot_binding", return_value=self.binding):
with self.assertRaises(m.ControlError) as e:
m.bind_screen(self.manifest)
self.assertEqual(e.exception.code, "SCREEN_CAPTURE_VT_MISMATCH")
def test_bind_node_mismatch(self):
self.manifest["node"] = "OTHER"
with patch.object(m, "snapshot_binding", return_value=self.binding):
with self.assertRaises(m.ControlError) as e:
m.bind_screen(self.manifest)
self.assertEqual(e.exception.code, "SCREEN_CAPTURE_NODE_MISMATCH")
def test_wrong_source(self):
self.manifest["source"] = "rendered-text"
with self.assertRaises(m.ControlError) as e:
m.bind_screen(self.manifest)
self.assertEqual(e.exception.code, "SCREEN_CONTROL_MODE_UNSUPPORTED")
def test_literal_text_no_shell_or_enter(self):
text = "printf '%s' '$(touch never)'"
with patch.object(m, "snapshot_binding", return_value=self.binding), patch.object(m, "_tmux", return_value="") as send:
result = m.act(self.manifest, "text.type", text=text)
self.assertTrue(result["performed"])
send.assert_called_once_with(self.binding["socket"]["path"], "send-keys", "-t", "%1", "-l", "--", text)
def test_explicit_key(self):
with patch.object(m, "snapshot_binding", return_value=self.binding), patch.object(m, "_tmux", return_value="") as send:
result = m.act(self.manifest, "key.send", key="ENTER")
self.assertTrue(result["performed"])
send.assert_called_once_with(self.binding["socket"]["path"], "send-keys", "-t", "%1", "Enter")
def test_text_newline_rejected(self):
with patch.object(m, "_tmux") as send:
with self.assertRaises(m.ControlError) as e:
m.act(self.manifest, "text.type", text="pwd\n")
self.assertEqual(e.exception.code, "SCREEN_TEXT_CONTROL_CHARACTER")
send.assert_not_called()
def test_unknown_key_rejected(self):
with patch.object(m, "_tmux") as send:
with self.assertRaises(m.ControlError):
m.act(self.manifest, "key.send", key="ALT_F2")
send.assert_not_called()
def test_changed_vt_no_input(self):
self.refused(lambda b: b.update(active_vt="tty2"))
def test_changed_pane_no_input(self):
self.refused(lambda b: b["pane"].update(pid=222))
def test_pid_reuse_no_input(self):
self.refused(lambda b: b["pane"].update(start_ticks=23))
def test_changed_socket_no_input(self):
self.refused(lambda b: b["socket"].update(inode=13))
def test_changed_geometry_no_input(self):
self.refused(lambda b: b.update(pane_geometry=[0, 0, 240, 66]))
def test_hidden_client_no_fallback(self):
identity = {"path": "/run/user/1000/console.sock", "inode": 12}
client_row = "101|/dev/tty2|$0|240|67|0\n"
with patch.object(m, "_active_vt", return_value="tty1"), patch.object(m, "_server_sockets", return_value=[(identity, {"pid": 100})]), patch.object(m, "_tmux", return_value=client_row) as run:
with self.assertRaises(m.ControlError) as e:
m.snapshot_binding()
self.assertEqual(e.exception.code, "SCREEN_VISIBLE_TMUX_NOT_FOUND")
self.assertEqual(run.call_count, 1)
def test_postflight_change_reports_performed(self):
other = copy.deepcopy(self.binding)
other["pane_id"] = "%2"
with patch.object(m, "snapshot_binding", side_effect=[self.binding, self.binding, other]), patch.object(m, "_tmux", return_value="") as send:
with self.assertRaises(m.ControlError) as e:
m.act(self.manifest, "key.send", key="ENTER")
self.assertEqual(e.exception.code, "SCREEN_CHANGED_AFTER_INPUT")
self.assertTrue(e.exception.details["performed"])
send.assert_called_once()
def test_tampered_capture_no_input(self):
self.path.write_bytes(b"changed")
with patch.object(m, "_tmux") as send:
with self.assertRaises(m.ControlError) as e:
m.act(self.manifest, "key.send", key="ENTER")
self.assertEqual(e.exception.code, "SCREEN_CAPTURE_SHA256_MISMATCH")
send.assert_not_called()
def test_capture_id_required(self):
self.manifest.pop("capture_id")
with self.assertRaises(m.ControlError) as e:
m.act(self.manifest, "key.send", key="ENTER")
self.assertEqual(e.exception.code, "SCREEN_CAPTURE_ID_REQUIRED")
def test_copy_mode_does_not_type_into_hidden_application(self):
self.binding["pane_in_mode"] = True
self.manifest["binding"] = copy.deepcopy(self.binding)
self.refused(lambda b: None, "SCREEN_TMUX_MODE_INPUT_UNSUPPORTED")
def test_optional_explicit_age_limit(self):
self.manifest["completed_at"] = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
with self.assertRaises(m.ControlError) as e:
m.act(self.manifest, "key.send", key="ENTER", max_age_s=30)
self.assertEqual(e.exception.code, "SCREEN_CAPTURE_EXPIRED")
def test_no_arbitrary_age_expiry_by_default(self):
self.manifest["completed_at"] = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
with patch.object(m, "snapshot_binding", return_value=self.binding), patch.object(m, "_tmux", return_value=""):
self.assertTrue(m.act(self.manifest, "key.send", key="ENTER")["performed"])
if __name__ == "__main__":
unittest.main()
+315
View File
@@ -0,0 +1,315 @@
import base64
import hashlib
import importlib.util
import json
import math
import random
import shlex
import struct
import unittest
import zlib
from pathlib import Path
from framebridge_expanded.real_screen.screen_frontdoor import CHUNK_BYTES, ScreenClient, ScreenError, register_framebridge_screen_tools
CAPTURE_ID = "1" * 32
SESSION_ID = "screen_" + "2" * 32
NODE_ID = "12345678-1234-1234-1234-123456789abc"
OTHER_NODE_ID = "87654321-4321-4321-4321-cba987654321"
def png(width=100, height=40):
def chunk(kind, data):
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
source = random.Random(20261022)
scanlines = b"".join(b"\0" + source.randbytes(width * 3) for _ in range(height))
return (b"\x89PNG\r\n\x1a\n" +
chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) +
chunk(b"IDAT", zlib.compress(scanlines)) + chunk(b"IEND", b""))
class Image:
def __init__(self, data, format):
self.data = data
self.format = format
class MCP:
def __init__(self):
self.tools = {}
self.annotations = {}
def tool(self, annotations):
def register(fn):
self.tools[fn.__name__] = fn
self.annotations[fn.__name__] = annotations
return fn
return register
class Executor:
def __init__(self, variant=None, hostname="fb-central-lab"):
self.data = png()
self.variant = variant
self.hostname = hostname
self.requests = []
def __call__(self, **kwargs):
request = json.loads(base64.b64decode(shlex.split(kwargs["script"])[-1], validate=True))
self.requests.append(request)
digest = hashlib.sha256(self.data).hexdigest()
manifest = {"capture_id": CAPTURE_ID, "node": self.hostname,
"bytes": len(self.data), "sha256": digest, "width": 100, "height": 40,
"source": "linux-framebuffer", "screen_type": "tty", "active_vt": "tty1",
"session_id": SESSION_ID, "full_screen": True, "authentic_pixels": True,
"path": "/private/internal.png"}
if request.get("session_id"):
manifest["requested_session_visible"] = request["session_id"] == SESSION_ID
if self.variant == "node":
manifest["node"] = "invalid hostname"
elif self.variant == "source":
manifest["source"] = "terminal-text-renderer"
elif self.variant == "provenance":
manifest["authentic_pixels"] = False
elif self.variant == "dimensions":
manifest["width"] = 99
elif self.variant == "x11":
manifest["source"] = "x11-root-XGetImage"
manifest["screen_type"] = "x11"
if request["op"] == "read_chunk":
index = request["index"]
data = self.data[index * CHUNK_BYTES:(index + 1) * CHUNK_BYTES]
result = {"capture_id": CAPTURE_ID, "index": index,
"total_chunks": math.ceil(len(self.data) / CHUNK_BYTES),
"bytes": len(self.data), "sha256": digest, "encoding": "base64",
"data": base64.b64encode(data).decode(),
"more": (index + 1) * CHUNK_BYTES < len(self.data)}
if self.variant == "chunk_index":
result["index"] = index + 1
elif self.variant == "chunk_hash":
result["sha256"] = "0" * 64
elif self.variant == "chunk_more":
result["more"] = not result["more"]
elif self.variant == "chunk_encoding":
result["data"] = "?invalid?"
elif self.variant == "chunk_length":
result["data"] = base64.b64encode(data[:-1]).decode()
elif self.variant == "image_hash":
changed = bytes([data[0] ^ 1]) + data[1:]
result["data"] = base64.b64encode(changed).decode()
else:
result = manifest
error = (self.variant in ("error_exit1", "uncertain_input") or
request["op"] in ("key", "text") and
request.get("session_id", SESSION_ID) != SESSION_ID)
envelope = {"ok": False, "error": {"code": "SESSION_NOT_VISIBLE"}} if error else {"ok": True, "result": result}
if self.variant == "uncertain_input":
envelope["error"] = {"code": "INPUT_SENT_OBSERVE_FAILED", "performed": True,
"capture_consumed": True, "observe_again": True,
"capture_id": CAPTURE_ID, "cause": "SCREEN_IO_FAILED",
"text": "MUST NOT ESCAPE"}
outer = {"ok": not error, "exit_code": "1" if error else "0",
"target": kwargs["target"], "node_id": NODE_ID, "mcp_job_id": kwargs["job_id"],
"mcp_script_sha256": kwargs["sha256"],
"stdout": json.dumps(envelope), "stdout_truncated": self.variant == "truncated"}
if self.variant == "outer_job":
outer["mcp_job_id"] = "different"
elif self.variant == "outer_hash":
outer["mcp_script_sha256"] = "0" * 64
elif self.variant == "outer_target":
outer["target"] = "OTHER"
elif self.variant == "outer_target_chunk" and request["op"] == "read_chunk":
outer["target"] = "OTHER"
elif self.variant == "outer_node_id_chunk" and request["op"] == "read_chunk":
outer["node_id"] = OTHER_NODE_ID
elif self.variant == "outer_node_id_missing":
outer.pop("node_id")
elif self.variant == "outer_node_id_invalid":
outer["node_id"] = "invalid"
return outer
class ScreenTests(unittest.TestCase):
def client(self, variant=None):
executor = Executor(variant)
return ScreenClient(executor, Image), executor
def test_native_image_return_preserves_bytes_and_omits_internal_path(self):
client, executor = self.client()
result = client.call("observe", "gerameg", True)
self.assertEqual(result[0]["node"], "GERAMEG")
self.assertEqual(result[1].data, executor.data)
self.assertEqual(result[1].format, "png")
self.assertNotIn("path", result[0]["result"])
self.assertNotIn("image_base64", json.dumps(result[0]))
self.assertGreater(len(executor.requests), 2)
def test_authorization_required_before_any_exec(self):
client, executor = self.client()
self.assertEqual(client.call("observe", "GERAMEG", False)["error"], "AUTHORIZED_TRUE_REQUIRED")
self.assertEqual(executor.requests, [])
def test_invalid_node_cannot_reach_shell(self):
client, executor = self.client()
self.assertEqual(client.call("observe", "GERAMEG;id", True)["error"], "SCREEN_NODE_INVALID")
self.assertEqual(executor.requests, [])
def test_visible_action_requires_capture_id(self):
client, executor = self.client()
self.assertEqual(client.call("key", "GERAMEG", True, key="ENTER")["error"], "SCREEN_CAPTURE_ID_REQUIRED")
self.assertEqual(executor.requests, [])
def test_hidden_reference_observe_shows_full_screen_and_visibility_false(self):
client, executor = self.client()
result = client.call("observe", "GERAMEG", True, session_id="hidden_virtual_session")
self.assertFalse(result[0]["result"]["requested_session_visible"])
self.assertIsInstance(result[1], Image)
def test_hidden_action_error_survives_exit1_without_image(self):
client, executor = self.client()
result = client.call("key", "GERAMEG", True, capture_id=CAPTURE_ID,
session_id="hidden_virtual_session", key="ENTER")
self.assertEqual(result["error"], "SESSION_NOT_VISIBLE")
self.assertEqual(len(executor.requests), 1)
def test_backend_structured_error_survives_failure_flag_and_exit1(self):
client, executor = self.client("error_exit1")
self.assertEqual(client.call("observe", "GERAMEG", True)["error"], "SESSION_NOT_VISIBLE")
def test_key_action_returns_new_real_image(self):
client, executor = self.client()
result = client.call("key", "GERAMEG", True, capture_id=CAPTURE_ID, key="CTRL_L")
self.assertEqual(executor.requests[0]["op"], "key")
self.assertEqual(result[1].data, executor.data)
def test_uncertain_input_details_preserved_without_payload(self):
client, executor = self.client("uncertain_input")
result = client.call("key", "GERAMEG", True, capture_id=CAPTURE_ID, key="ENTER")
self.assertEqual(result["error"], "INPUT_SENT_OBSERVE_FAILED")
self.assertIs(result["performed"], True)
self.assertIs(result["capture_consumed"], True)
self.assertIs(result["observe_again"], True)
self.assertEqual(result["capture_id"], CAPTURE_ID)
self.assertNotIn("text", result)
self.assertEqual(len(executor.requests), 1)
def test_utf8_text_limit_matches_backend(self):
client, executor = self.client()
result = client.call("text", "GERAMEG", True, capture_id=CAPTURE_ID, text="界" * 1400)
self.assertEqual(result["error"], "SCREEN_TEXT_INVALID")
self.assertEqual(executor.requests, [])
def test_text_is_base64_transport_and_not_shell_interpolation(self):
client, executor = self.client()
text = "literal $(touch /tmp/no) `id` ' quote"
result = client.call("text", "GERAMEG", True, capture_id=CAPTURE_ID, text=text)
self.assertEqual(executor.requests[0]["text"], text)
self.assertIsInstance(result[1], Image)
def test_integrity_rejections(self):
expected = {"outer_job": "SCREEN_EXEC_BINDING_INVALID",
"outer_hash": "SCREEN_EXEC_BINDING_INVALID",
"outer_target": "SCREEN_EXEC_BINDING_INVALID",
"truncated": "SCREEN_STDOUT_INVALID", "node": "SCREEN_IMAGE_HOSTNAME_INVALID",
"outer_target_chunk": "SCREEN_EXEC_BINDING_INVALID",
"outer_node_id_chunk": "SCREEN_CHUNK_NODE_MISMATCH",
"outer_node_id_missing": "SCREEN_EXEC_NODE_ID_INVALID",
"outer_node_id_invalid": "SCREEN_EXEC_NODE_ID_INVALID",
"source": "SCREEN_PIXEL_SOURCE_INVALID", "provenance": "SCREEN_PIXEL_PROVENANCE_INVALID",
"dimensions": "SCREEN_PNG_INVALID", "chunk_index": "SCREEN_CHUNK_BINDING_INVALID",
"chunk_hash": "SCREEN_CHUNK_MANIFEST_INVALID", "chunk_more": "SCREEN_CHUNK_MANIFEST_INVALID",
"chunk_encoding": "SCREEN_CHUNK_INVALID", "chunk_length": "SCREEN_CHUNK_LENGTH_INVALID",
"image_hash": "SCREEN_IMAGE_HASH_MISMATCH"}
for variant, code in expected.items():
with self.subTest(variant=variant):
client, _ = self.client(variant)
self.assertEqual(client.call("observe", "GERAMEG", True)["error"], code)
def test_generic_node_is_passed_to_existing_executor(self):
client, executor = self.client()
self.assertEqual(client.call("observe", "ASUS", True)[0]["node"], "ASUS")
def test_exact_x11_capture_source_is_accepted(self):
client, executor = self.client("x11")
result = client.call("observe", "ASUS", True)
self.assertEqual(result[0]["result"]["source"], "x11-root-XGetImage")
self.assertEqual(result[1].data, executor.data)
def test_trusted_alias_or_uuid_resolver_is_injected(self):
executor = Executor()
client = ScreenClient(executor, Image, lambda node: "GERAMEG")
self.assertEqual(client.call("observe", "node-alias", True)[0]["node"], "GERAMEG")
def test_registration_annotations_and_callable_functions(self):
mcp = MCP()
register_framebridge_screen_tools(mcp, Executor(), image_factory=Image)
self.assertEqual(set(mcp.tools), {"framebridge_screen_observe", "framebridge_screen_key", "framebridge_screen_text"})
self.assertTrue(mcp.annotations["framebridge_screen_observe"]["readOnlyHint"])
self.assertFalse(mcp.annotations["framebridge_screen_key"]["readOnlyHint"])
self.assertIsInstance(mcp.tools["framebridge_screen_observe"]("GERAMEG", True)[1], Image)
def test_local_route_preserves_distinct_real_hostname_and_uuid(self):
executor = Executor(hostname="fb-central-lab")
client = ScreenClient(executor, Image, lambda alias: "LOCAL")
result = client.call("observe", "LOCAL", True)
self.assertEqual(result[0]["node"], "LOCAL")
self.assertEqual(result[0]["hostname"], "fb-central-lab")
self.assertEqual(result[0]["node_id"], NODE_ID)
self.assertEqual(result[0]["result"]["node"], "fb-central-lab")
self.assertEqual(result[0]["result"]["transport_node"], "LOCAL")
self.assertEqual(result[1].data, executor.data)
def test_remote_uuid_alias_and_hostname_remain_distinct(self):
executor = Executor(hostname="fb-node-lab")
client = ScreenClient(executor, Image, lambda alias: "fb-node")
result = client.call("observe", NODE_ID, True)
self.assertEqual(result[0]["requested_node"], NODE_ID)
self.assertEqual(result[0]["node"], "fb-node")
self.assertEqual(result[0]["hostname"], "fb-node-lab")
self.assertEqual(result[0]["result"]["node"], "fb-node-lab")
self.assertEqual(result[0]["result"]["transport_node"], "fb-node")
self.assertEqual(result[1].data, executor.data)
def test_screen_action_keeps_capture_and_session_with_distinct_hostname(self):
executor = Executor(hostname="fb-node-lab")
client = ScreenClient(executor, Image, lambda alias: "fb-node")
result = client.call("key", "fb-node", True, capture_id=CAPTURE_ID,
session_id=SESSION_ID, key="ENTER")
self.assertEqual(executor.requests[0]["capture_id"], CAPTURE_ID)
self.assertEqual(executor.requests[0]["session_id"], SESSION_ID)
self.assertEqual(result[0]["hostname"], "fb-node-lab")
self.assertEqual(result[1].data, executor.data)
def test_arbitrary_dictionary_cannot_be_treated_as_authenticated_manifest(self):
client, _ = self.client()
verified = client.remote("LOCAL", {"op": "observe"})
with self.assertRaisesRegex(ScreenError, "SCREEN_IMAGE_NODE_MISMATCH"):
client.image("LOCAL", dict(verified))
def test_another_client_cannot_supply_authenticated_manifest(self):
first, _ = self.client()
second, _ = self.client()
verified = first.remote("LOCAL", {"op": "observe"})
with self.assertRaisesRegex(ScreenError, "SCREEN_IMAGE_NODE_MISMATCH"):
second.image("LOCAL", verified)
def test_authenticated_hostname_cannot_be_changed_after_transport_validation(self):
client, _ = self.client()
verified = client.remote("LOCAL", {"op": "observe"})
verified["node"] = "forged-hostname"
with self.assertRaisesRegex(ScreenError, "SCREEN_IMAGE_NODE_MISMATCH"):
client.image("LOCAL", verified)
def test_hostname_must_be_a_bounded_nonempty_string(self):
for hostname in ("", None, 123, "invalid hostname", "x" * 81):
with self.subTest(hostname=hostname):
executor = Executor(hostname=hostname)
client = ScreenClient(executor, Image, lambda alias: "LOCAL")
self.assertEqual(client.call("observe", "LOCAL", True)["error"],
"SCREEN_IMAGE_HOSTNAME_INVALID")
if __name__ == "__main__":
unittest.main()
+161
View File
@@ -0,0 +1,161 @@
"""Exercise original-artifact replay and the actual bundled MCP SDK contract."""
import asyncio
import base64
import hashlib
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from mcp.server.fastmcp import FastMCP, Image
from mcp.types import CallToolResult
from framebridge_expanded.real_screen import screen_rpc as rpc
from framebridge_expanded.real_screen.screen_capture import CaptureError
from framebridge_expanded.real_screen.screen_frontdoor import ScreenClient
from framebridge_expanded.real_screen.screen_presentation import (
TEMPLATE_URI, VIEWER_HTML, register_screen_presentation,
)
from test_screen_frontdoor import Executor, CAPTURE_ID, NODE_ID, png
class SavedCaptureRpcTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.directory = Path(self.temp.name)
self.data = png()
self.manifest = {
"capture_id": CAPTURE_ID, "node": "fb-central-lab", "uid": os.getuid(),
"source": "linux-framebuffer", "screen_type": "tty", "active_vt": "tty1",
"bytes": len(self.data), "sha256": hashlib.sha256(self.data).hexdigest(),
"width": 100, "height": 40, "synthetic": False, "pixel_source_verified": True,
"full_screen": True, "authentic_pixels": True, "control_available": True,
"_action_consumed": True, "captured_at": "2026-10-24T12:00:00Z",
}
self.png = self.directory / (CAPTURE_ID + ".png")
self.png.write_bytes(self.data)
self.png.chmod(0o600)
for suffix in (".json", ".bound.json"):
rpc.write_record(self.directory / (CAPTURE_ID + suffix), self.manifest)
self.initial = {p.name: (p.read_bytes(), p.stat().st_mtime_ns)
for p in self.directory.iterdir()}
def test_replay_survives_client_restart_and_consumed_input_without_observation(self):
with patch.object(rpc, "observe", side_effect=AssertionError("new capture forbidden")), \
patch.object(rpc, "act", side_effect=AssertionError("input forbidden")):
result = rpc.dispatch({"op": "read_capture", "capture_id": CAPTURE_ID}, self.directory)
self.assertTrue(result["ok"])
result = result["result"]
self.assertEqual(result["sha256"], self.manifest["sha256"])
self.assertEqual(result["captured_at"], self.manifest["captured_at"])
self.assertTrue(result["historical_capture"])
self.assertFalse(result["control_available"])
self.assertNotIn("_action_consumed", result)
self.assertNotIn("path", result)
self.assertEqual(self.initial, {p.name: (p.read_bytes(), p.stat().st_mtime_ns)
for p in self.directory.iterdir()})
def test_replay_rejects_changed_png(self):
self.png.write_bytes(self.data + b"corruption")
with self.assertRaisesRegex(CaptureError, "CAPTURE_SHA256_MISMATCH"):
rpc.dispatch({"op": "read_capture", "capture_id": CAPTURE_ID}, self.directory)
def test_replay_rejects_symlink_record(self):
record = self.directory / (CAPTURE_ID + ".bound.json")
record.unlink()
record.symlink_to(self.directory / (CAPTURE_ID + ".json"))
with self.assertRaises((OSError, rpc.ScreenRpcError)):
rpc.dispatch({"op": "read_capture", "capture_id": CAPTURE_ID}, self.directory)
def test_replay_rejects_unrecognized_fields_and_unknown_reference(self):
with self.assertRaisesRegex(rpc.ScreenRpcError, "UNKNOWN_REQUEST_FIELDS"):
rpc.validate_request({"op": "read_capture", "capture_id": CAPTURE_ID, "text": "x"})
with self.assertRaises((OSError, rpc.ScreenRpcError)):
rpc.dispatch({"op": "read_capture", "capture_id": "f" * 32}, self.directory)
class ScreenPresentationSdkTests(unittest.TestCase):
def setUp(self):
self.executor = Executor()
self.client = ScreenClient(self.executor, Image, lambda name: name)
self.mcp = FastMCP("screen-presentation-test")
self.show = register_screen_presentation(self.mcp, self.client)
def test_actual_sdk_preserves_original_png_and_private_ui_metadata(self):
response = asyncio.run(self.mcp.call_tool(
"framebridge_screen_show",
{"node": "LOCAL", "capture_id": CAPTURE_ID, "authorized": True}))
self.assertIsInstance(response, CallToolResult)
images = [c for c in response.content if c.type == "image"]
self.assertEqual(len(images), 1)
self.assertEqual(images[0].mimeType, "image/png")
self.assertEqual(base64.b64decode(images[0].data), self.executor.data)
self.assertEqual(response.meta["framebridgeImage"]["data"], images[0].data)
self.assertEqual(response.structuredContent["node_id"], NODE_ID)
self.assertEqual(response.structuredContent["result"]["capture_id"], CAPTURE_ID)
self.assertEqual(response.structuredContent["result"]["sha256"],
hashlib.sha256(self.executor.data).hexdigest())
self.assertEqual(self.executor.requests[0],
{"op": "read_capture", "capture_id": CAPTURE_ID})
self.assertTrue(all(r["op"] in {"read_capture", "read_chunk"} for r in self.executor.requests))
def test_actual_descriptor_declares_ui_readonly_and_structured_schema(self):
tools = asyncio.run(self.mcp.list_tools())
self.assertEqual(len(tools), 1)
tool = tools[0]
self.assertEqual(tool.meta["ui"]["resourceUri"], TEMPLATE_URI)
self.assertEqual(tool.meta["openai/outputTemplate"], TEMPLATE_URI)
self.assertTrue(tool.annotations.readOnlyHint)
self.assertFalse(tool.annotations.destructiveHint)
self.assertIn("ok", tool.outputSchema["properties"])
self.assertIn("result", tool.outputSchema["properties"])
def test_resource_contains_offline_viewer_and_integrity_validation(self):
resources = asyncio.run(self.mcp.list_resources())
self.assertEqual(len(resources), 1)
self.assertEqual(str(resources[0].uri), TEMPLATE_URI)
contents = list(asyncio.run(self.mcp.read_resource(TEMPLATE_URI)))
self.assertEqual(contents[0].mime_type, "text/html;profile=mcp-app")
self.assertEqual(contents[0].content, VIEWER_HTML)
self.assertEqual(contents[0].meta["ui"]["csp"]["connectDomains"], [])
self.assertNotIn("https://", VIEWER_HTML)
self.assertNotIn("fetch(", VIEWER_HTML)
self.assertIn('crypto.subtle.digest("SHA-256",bytes)', VIEWER_HTML)
self.assertIn("ui/notifications/tool-result", VIEWER_HTML)
self.assertIn("window.openai", VIEWER_HTML)
self.assertIn("This saved capture does not show subsequent screen changes", VIEWER_HTML)
def test_unauthorized_replay_never_calls_backend(self):
response = self.show("LOCAL", CAPTURE_ID, False)
self.assertTrue(response.isError)
self.assertEqual(response.structuredContent["error"], "AUTHORIZED_TRUE_REQUIRED")
self.assertEqual(self.executor.requests, [])
def test_replay_requires_exact_capture_reference(self):
response = self.show("LOCAL", "f" * 32, True)
self.assertTrue(response.isError)
self.assertEqual(response.structuredContent["error"], "SCREEN_CAPTURE_REFERENCE_MISMATCH")
self.assertEqual(len(self.executor.requests), 1)
def test_replay_rejects_invalid_capture_before_backend(self):
response = self.show("LOCAL", "../invalid", True)
self.assertTrue(response.isError)
self.assertEqual(response.structuredContent["error"], "SCREEN_CAPTURE_ID_REQUIRED")
self.assertEqual(self.executor.requests, [])
def test_replay_preserves_existing_uuid_binding_validation(self):
def corrupted(**kwargs):
result = self.executor(**kwargs)
result.pop("node_id", None)
return result
response = register_screen_presentation(FastMCP("binding-test"), ScreenClient(corrupted, Image))(
"LOCAL", CAPTURE_ID, True)
self.assertTrue(response.isError)
self.assertFalse(response.structuredContent["ok"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,23 @@
import unittest
from unittest.mock import patch
from framebridge_expanded import fleet
from framebridge_expanded.real_screen.screen_frontdoor import resolve_profile_node, ScreenError
class CanonicalScreenNodeTests(unittest.TestCase):
def test_remote_uuid_is_bound_to_confirmed_executor_alias(self):
uid = "12345678-1234-1234-1234-123456789abc"
with patch.object(fleet, "resolve_node", return_value={"transport": "broker_outbound", "alias": "worker-1", "node_id": uid}) as resolve:
self.assertEqual(resolve_profile_node(uid), "worker-1")
resolve.assert_called_once_with(uid)
def test_receiver_identity_is_canonical_local(self):
with patch.object(fleet, "resolve_node", return_value={"transport": "local", "alias": "central"}):
self.assertEqual(resolve_profile_node("central"), "LOCAL")
def test_unknown_alias_has_no_hostname_or_ssh_fallback(self):
with patch.object(fleet, "resolve_node", side_effect=fleet.FleetError("NODE_NOT_FOUND")):
with self.assertRaisesRegex(ScreenError, "NODE_NOT_FOUND"):
resolve_profile_node("unconfirmed-host")
if __name__ == "__main__":
unittest.main()
+106
View File
@@ -0,0 +1,106 @@
"""Exercise the actual portable screen registration callback and its UUID contract.
The bounded executor is a fixture. This is not VM, GUI, TTY or input acceptance.
"""
import ast
from pathlib import Path
import unittest
from framebridge_expanded.real_screen.screen_frontdoor import register_framebridge_screen_tools
from test_screen_frontdoor import Executor, Image, MCP, NODE_ID
PORTABLE = Path(__file__).resolve().parents[1] / "portable/front_door_mcp.py"
class PortableScreenRegistrationTests(unittest.TestCase):
def registration(self, target="LOCAL", corrupt=None):
fixture = Executor(hostname="fb-central-lab" if target == "LOCAL" else "fb-node-lab")
calls, legacy_calls = [], []
def expanded(alias, script, authorized, timeout_s=120, job_id="", sha256="", include_stdout=False):
args = dict(script=script, authorized=authorized, timeout_s=timeout_s,
job_id=job_id, sha256=sha256, include_stdout=include_stdout, target=alias)
calls.append(args)
result = fixture(**args)
if corrupt == "uuid":
result.pop("node_id")
elif corrupt == "hash":
result["mcp_script_sha256"] = "0" * 64
return result
def legacy(**kwargs):
# Model the real legacy LOCAL envelope which lacks the Fleet UUID.
legacy_calls.append(kwargs)
if kwargs["target"] != "LOCAL":
remote = dict(kwargs)
return expanded(remote.pop("target"), **remote)
result = fixture(**kwargs)
result.pop("node_id")
return result
tree = ast.parse(PORTABLE.read_text())
definitions = [item for item in tree.body
if isinstance(item, ast.FunctionDef) and item.name == "_screen_exec_request"]
registrations = [item for item in tree.body
if isinstance(item, ast.Assign) and any(
isinstance(name, ast.Name) and name.id == "_real_screen_client"
for name in item.targets)]
self.assertEqual(len(registrations), 1)
scope = dict(mcp=MCP(), Image=Image, expanded_execute_node=expanded, expanded_execute_screen_node=expanded,
framebridge_exec=legacy,
register_framebridge_screen_tools=register_framebridge_screen_tools,
resolve_profile_node=lambda node: node)
# Load the definition and registration from production source rather
# than recreating the adapter inside this test.
module = ast.Module(body=definitions + registrations, type_ignores=[])
exec(compile(ast.fix_missing_locations(module), str(PORTABLE), "exec"), scope)
return scope["mcp"], fixture, calls, legacy_calls, scope["_real_screen_client"]
def assert_observe(self, target):
mcp, fixture, calls, legacy_calls, _ = self.registration(target)
response = mcp.tools["framebridge_screen_observe"](target, True)
self.assertIsInstance(response, list)
metadata, image = response
self.assertTrue(metadata["ok"])
self.assertEqual(metadata["requested_node"], target)
self.assertEqual(metadata["node_id"], NODE_ID)
self.assertEqual(metadata["hostname"], fixture.hostname)
self.assertEqual(image.data, fixture.data)
self.assertEqual(image.format, "png")
if target == "LOCAL":
self.assertFalse(legacy_calls)
self.assertGreater(len(calls), 1) # Manifest plus bound image chunks.
self.assertTrue(all(item["target"] == target for item in calls))
self.assertTrue(all(item["authorized"] is True and item["include_stdout"] is True
and item["timeout_s"] == 30 for item in calls))
def test_actual_registration_preserves_local_fleet_uuid(self):
self.assert_observe("LOCAL")
def test_actual_registration_preserves_remote_alias_and_hostname(self):
self.assert_observe("fb-node")
def test_actual_registration_still_rejects_missing_uuid(self):
mcp, _, _, _, _ = self.registration(corrupt="uuid")
response = mcp.tools["framebridge_screen_observe"]("LOCAL", True)
self.assertFalse(response["ok"])
self.assertEqual(response["error"], "SCREEN_EXEC_NODE_ID_INVALID")
def test_actual_registration_still_rejects_wrong_script_binding(self):
mcp, _, _, _, _ = self.registration(corrupt="hash")
response = mcp.tools["framebridge_screen_observe"]("LOCAL", True)
self.assertFalse(response["ok"])
self.assertEqual(response["error"], "SCREEN_EXEC_BINDING_INVALID")
def test_unauthorized_observation_never_reaches_executor(self):
mcp, _, calls, legacy_calls, _ = self.registration()
response = mcp.tools["framebridge_screen_observe"]("LOCAL", False)
self.assertFalse(response["ok"])
self.assertEqual(response["error"], "AUTHORIZED_TRUE_REQUIRED")
self.assertFalse(calls)
self.assertFalse(legacy_calls)
if __name__ == "__main__":
unittest.main()
+127
View File
@@ -0,0 +1,127 @@
import base64
import hashlib
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from framebridge_expanded.real_screen import screen_rpc as rpc
class ScreenRpcTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.directory = Path(self.temp.name)
self.counter = 0
self.binding = {'backend': 'visible-tmux', 'pane_id': '%1', 'node': 'TEST', 'uid': os.getuid()}
self.actions = []
self.patches = [
patch.object(rpc, 'snapshot_binding', return_value=self.binding),
patch.object(rpc, 'capture_screen', side_effect=self.capture),
patch.object(rpc, 'bind_screen', side_effect=lambda m: {**m, 'binding': self.binding}),
patch.object(rpc, 'read_chunk', side_effect=self.chunk),
patch.object(rpc, 'act', side_effect=self.action),
patch.object(rpc.time, 'sleep'),
]
for p in self.patches:
p.start()
self.addCleanup(p.stop)
def capture(self, directory):
self.counter += 1
capture_id = f'{self.counter:032x}'
data = b'actual-pixel-test-fixture-' + capture_id.encode()
path = directory / (capture_id + '.png')
path.write_bytes(data)
return {'capture_id': capture_id, 'source': 'linux-framebuffer', 'screen_type': 'tty',
'node': 'TEST', 'uid': os.getuid(), 'width': 1920, 'height': 1080,
'path': str(path), 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest(),
'active_vt': 'tty1', 'synthetic': False, 'pixel_source_verified': True}
def chunk(self, directory, capture_id, index, **kwargs):
data = (directory / (capture_id + '.png')).read_bytes()
digest = hashlib.sha256(data).hexdigest()
if digest != kwargs['expected_sha256']:
raise rpc.CaptureError('CAPTURE_SHA256_MISMATCH')
return {'capture_id': capture_id, 'index': index, 'bytes': len(data), 'sha256': digest,
'total_chunks': 1, 'encoding': 'base64', 'data': base64.b64encode(data).decode(), 'more': False}
def action(self, manifest, kind, **kwargs):
self.actions.append((manifest['capture_id'], kind, kwargs))
return {'performed': True}
def observe(self, **kwargs):
return rpc.dispatch({'op': 'observe', **kwargs}, self.directory)['result']
def test_observation_uses_capture_pixels_not_terminal_text(self):
m = self.observe(session_id='hidden_terminal')
self.assertTrue(m['full_screen'] and m['authentic_pixels'])
self.assertFalse(m['requested_session_visible'])
self.assertNotIn('path', m)
self.assertTrue(m['control_available'])
self.assertEqual(m['width'], 1920)
def test_hidden_terminal_id_cannot_receive_input_for_visible_image(self):
m = self.observe()
with self.assertRaisesRegex(rpc.ScreenRpcError, 'SESSION_NOT_VISIBLE'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'session_id': 'hidden_terminal', 'key': 'ENTER'}, self.directory)
self.assertEqual(self.actions, [])
def test_input_returns_new_observation_and_cannot_replay_old_capture(self):
m = self.observe()
after = rpc.dispatch({'op': 'text', 'capture_id': m['capture_id'], 'session_id': m['session_id'], 'text': 'visible marker'}, self.directory)['result']
self.assertNotEqual(after['capture_id'], m['capture_id'])
self.assertEqual(len(self.actions), 1)
with self.assertRaisesRegex(rpc.ScreenRpcError, 'CAPTURE_ALREADY_USED'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
self.assertEqual(len(self.actions), 1)
def test_unknown_send_result_does_not_replay_input(self):
m = self.observe()
with patch.object(rpc, 'act', side_effect=rpc.ControlError('SCREEN_BINDING_CHANGED')):
with self.assertRaisesRegex(rpc.ScreenRpcError, 'SCREEN_BINDING_CHANGED'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
with self.assertRaisesRegex(rpc.ScreenRpcError, 'CAPTURE_ALREADY_USED'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
def test_target_change_during_capture_returns_no_bound_image(self):
with patch.object(rpc, 'bind_screen', side_effect=lambda m: {**m, 'binding': {**self.binding, 'pane_id': '%0'}}):
with self.assertRaisesRegex(rpc.ScreenRpcError, 'VISIBLE_TARGET_CHANGED_DURING_CAPTURE'):
self.observe()
self.assertEqual(list(self.directory.glob('*.bound.json')), [])
def test_sent_input_is_reported_if_following_capture_fails(self):
m = self.observe()
with patch.object(rpc, 'capture_screen', side_effect=rpc.CaptureError('ACTIVE_VT_CHANGED')):
with self.assertRaises(rpc.ScreenRpcError) as caught:
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
self.assertEqual(caught.exception.code, 'INPUT_SENT_OBSERVE_FAILED')
self.assertTrue(caught.exception.details['performed'])
self.assertEqual(len(self.actions), 1)
def test_view_can_succeed_without_safe_input_target(self):
with patch.object(rpc, 'snapshot_binding', side_effect=rpc.ControlError('SCREEN_VISIBLE_TMUX_NOT_FOUND')):
m = self.observe()
self.assertFalse(m['control_available'])
with self.assertRaisesRegex(rpc.ScreenRpcError, 'VISIBLE_INPUT_UNAVAILABLE'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
self.assertEqual(self.actions, [])
def test_changed_png_cannot_drive_input(self):
m = self.observe()
(self.directory / (m['capture_id'] + '.png')).write_bytes(b'different pixels')
with self.assertRaisesRegex(rpc.CaptureError, 'CAPTURE_SHA256_MISMATCH'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
self.assertEqual(self.actions, [])
def test_read_requires_expected_image_hash(self):
m = self.observe()
with self.assertRaisesRegex(rpc.ScreenRpcError, 'CAPTURE_SHA256_MISMATCH'):
rpc.dispatch({'op': 'read_chunk', 'capture_id': m['capture_id'], 'index': 0, 'expected_sha256': '0' * 64}, self.directory)
if __name__ == '__main__':
unittest.main()
+16
View File
@@ -0,0 +1,16 @@
import unittest
from pathlib import Path
from unittest.mock import patch
from framebridge_expanded.real_screen import screen_rpc as rpc
class X11InputRoutingTests(unittest.TestCase):
def test_x11_input_is_rejected_with_graphical_cua_instruction(self):
for op, value in (("text", {"text": "test"}), ("key", {"key": "ENTER"})):
request = {"op": op, "capture_id": "a" * 32, **value}
with self.subTest(op=op), patch.object(rpc, "load_capture", return_value={"screen_type": "x11"}), patch.object(rpc, "act") as hidden_action:
with self.assertRaisesRegex(rpc.ScreenRpcError, "USE_GRAPHICAL_CUA_FOR_X11_INPUT"):
rpc.dispatch(request, Path("/unused/fixture"))
hidden_action.assert_not_called()
if __name__ == "__main__":
unittest.main()
+379
View File
@@ -0,0 +1,379 @@
"""Regression tests for the explicit typed screen RPC transport."""
import ast
from pathlib import Path
import unittest
PORTABLE = Path(__file__).resolve().parents[1] / "portable/front_door_mcp.py"
class PortableTypedRouteTests(unittest.TestCase):
def test_screen_callback_uses_explicit_typed_route(self):
calls = []
def typed(*args):
calls.append(("typed", args))
return {"typed": True}
def generic(*args):
calls.append(("generic", args))
return {"typed": False}
source = ast.parse(PORTABLE.read_text())
definitions = [n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "_screen_exec_request"]
self.assertEqual(len(definitions), 1)
scope = {"expanded_execute_node": generic, "expanded_execute_screen_node": typed}
exec(compile(ast.Module(body=definitions, type_ignores=[]), str(PORTABLE), "exec"), scope)
result = scope["_screen_exec_request"](script="canonical-screen-fixture", authorized=True, timeout_s=30,
job_id="screen-fixture", sha256="fixture-hash",
include_stdout=True, target="worker-1")
self.assertEqual([c[0] for c in calls], ["typed"])
self.assertTrue(result["typed"])
import base64
import copy
import hashlib
import json
import os
import subprocess
import time
from types import SimpleNamespace
from unittest.mock import Mock, patch
from framebridge_expanded import fleet, broker, node
from framebridge_expanded.real_screen import screen_transport as transport
from framebridge_expanded.real_screen.screen_frontdoor import ScreenClient, ScreenError, RPC_COMMAND
from framebridge_expanded.real_screen.screen_rpc import encode
NODE_ID = "12345678-1234-1234-1234-123456789abc"
CAPTURE_ID = "a" * 32
def make_payload(request=None):
request = {"op": "observe"} if request is None else request
argument = base64.b64encode(encode(request)).decode("ascii")
script = RPC_COMMAND + argument + "\n"
return {"version": 1, "mode": "EXEC", "authorized": True,
"request_type": "screen_rpc", "script": script,
"sha256": hashlib.sha256(script.encode()).hexdigest(),
"job_id": "screen-fixture", "timeout_s": 1, "include_stdout": True}
def response(payload, envelope=None):
envelope = {"ok": True, "result": {"fixture": True}} if envelope is None else envelope
return {"ok": envelope["ok"], "job_id": payload["job_id"],
"command_sha256": payload["sha256"], "exit_code": "0" if envelope["ok"] else "1",
"stdout": encode(envelope).decode() + "\n", "stdout_truncated": False}
class TypedPayloadTests(unittest.TestCase):
def test_canonical_operation_roundtrips_without_shell(self):
payload = make_payload({"op": "status"})
request, argument = transport.validate_payload(payload)
self.assertEqual(request, {"op": "status"})
self.assertEqual(base64.b64decode(argument), encode(request))
def test_common_authorization_and_shape_rejections(self):
valid = make_payload()
mutations = [{"authorized": False}, {"version": True}, {"version": 2},
{"mode": "CUA"}, {"job_id": "invalid job"}, {"timeout_s": True},
{"timeout_s": 31}, {"timeout_s": 0}, {"include_stdout": False},
{"sha256": "0" * 64}, {"request_type": "SCREEN_RPC"},
{"unexpected": "not accepted"}]
for mutation in mutations:
with self.subTest(mutation=mutation):
payload = dict(valid, **mutation)
with self.assertRaises(transport.ScreenTransportError):
transport.validate_payload(payload)
payload = dict(valid)
del payload["authorized"]
with self.assertRaises(transport.ScreenTransportError):
transport.validate_payload(payload)
def test_no_noncanonical_script_or_unknown_operation(self):
prefix = RPC_COMMAND
bad = ["echo arbitrary\n", prefix + "not-base64\n",
make_payload()["script"] + "echo extra\n",
make_payload()["script"].rstrip("\n") + " extra\n",
prefix + base64.b64encode(b'{ "op" : "status" }').decode() + "\n",
prefix + base64.b64encode(encode({"op": "exec", "script": "echo bad"})).decode() + "\n",
prefix + base64.b64encode(encode({"op": "status", "path": "/tmp/other"})).decode() + "\n",
prefix + "'" + base64.b64encode(encode({"op": "status"})).decode() + "'\n",
prefix + base64.b64encode(b"x" * 16385).decode() + "\n"]
for script in bad:
with self.subTest(script=script[:100]):
payload = make_payload()
payload.update(script=script, sha256=hashlib.sha256(script.encode()).hexdigest())
with self.assertRaises(transport.ScreenTransportError):
transport.validate_payload(payload)
def test_existing_input_schema_is_preserved(self):
for request in [{"op": "key", "capture_id": CAPTURE_ID, "key": "ENTER"},
{"op": "text", "capture_id": CAPTURE_ID, "text": "printf"},
{"op": "read_chunk", "capture_id": CAPTURE_ID, "index": 0, "expected_sha256": "f" * 64}]:
with self.subTest(op=request["op"]):
parsed, _ = transport.validate_payload(make_payload(request))
self.assertEqual(parsed, request)
with self.assertRaises(transport.ScreenTransportError):
transport.validate_payload(make_payload({"op": "text", "capture_id": CAPTURE_ID, "text": "\x00"}))
def test_adapter_intercepts_only_explicit_type(self):
original = Mock(return_value={"visible_engine": True})
adapter = SimpleNamespace(handle=original)
transport.install_adapter_route(adapter)
generic = make_payload()
del generic["request_type"]
self.assertEqual(adapter.handle(generic), {"visible_engine": True})
original.assert_called_once_with(generic)
original.reset_mock()
with self.assertRaises(transport.ScreenTransportError):
adapter.handle(dict(make_payload(), authorized=False))
original.assert_not_called()
def test_malformed_typed_case_cannot_fall_back(self):
original = Mock(side_effect=AssertionError("no visible FBTERM fallback"))
adapter = SimpleNamespace(handle=original)
transport.install_adapter_route(adapter)
for mutation in [{"request_type": " SCREEN_RPC "}, {"script": "echo arbitrary\n"},
{"sha256": "0" * 64}, {"mode": "INVALID"}, {"version": False}]:
with self.subTest(mutation=mutation):
with self.assertRaises(transport.ScreenTransportError):
adapter.handle(dict(make_payload(), **mutation))
original.assert_not_called()
def test_success_and_child_error_envelopes_remain_exact(self):
payload = make_payload()
good = {"ok": True, "result": {"fixture": True}}
error = {"ok": False, "error": {"code": "INPUT_SENT_OBSERVE_FAILED", "performed": True,
"capture_consumed": True, "observe_again": True,
"capture_id": CAPTURE_ID, "cause": "SCREEN_IO_FAILED"}}
for envelope in [good, error]:
with self.subTest(ok=envelope["ok"]):
runner = Mock(return_value=(0 if envelope["ok"] else 1, encode(envelope) + b"\n", b""))
result = transport.handle_request(payload, runner=runner)
self.assertEqual(result["stdout"].encode(), encode(envelope) + b"\n")
self.assertEqual(result["job_id"], payload["job_id"])
self.assertEqual(result["command_sha256"], payload["sha256"])
self.assertEqual(result["ok"], envelope["ok"])
runner.assert_called_once_with(transport.validate_payload(payload)[1], 1)
def test_input_timeout_is_unknown_and_never_replayed(self):
payload = make_payload({"op": "key", "capture_id": CAPTURE_ID, "key": "ENTER"})
runner = Mock(side_effect=transport.ScreenProcessError("SCREEN_RPC_TIMEOUT", started=True))
result = transport.handle_request(payload, runner=runner)
error = json.loads(result["stdout"])["error"]
self.assertEqual(error["code"], "SCREEN_RPC_TIMEOUT")
self.assertTrue(error["observe_again"])
self.assertEqual(error["capture_id"], CAPTURE_ID)
self.assertNotIn("performed", error)
self.assertNotIn("capture_consumed", error)
runner.assert_called_once()
def test_prelaunch_or_readonly_timeout_does_not_invent_input(self):
for request, started in [({"op": "observe"}, True),
({"op": "key", "capture_id": CAPTURE_ID, "key": "ENTER"}, False)]:
with self.subTest(request=request, started=started):
runner = Mock(side_effect=transport.ScreenProcessError("SCREEN_RPC_PROCESS_FAILED", started=started))
result = transport.handle_request(make_payload(request), runner=runner)
error = json.loads(result["stdout"])["error"]
self.assertNotIn("performed", error)
self.assertNotIn("capture_consumed", error)
self.assertNotIn("observe_again", error)
runner.assert_called_once()
def test_invalid_or_oversized_input_reply_is_uncertain(self):
payload = make_payload({"op": "text", "capture_id": CAPTURE_ID, "text": "printf"})
for output in [b"not JSON\n", b"x" * 4097]:
with self.subTest(size=len(output)):
runner = Mock(return_value=(0, output, b""))
result = transport.handle_request(payload, runner=runner)
error = json.loads(result["stdout"])["error"]
self.assertTrue(error["observe_again"])
self.assertNotIn("performed", error)
self.assertNotIn("capture_consumed", error)
self.assertLess(len(result["stdout"].encode()), 4096)
runner.assert_called_once()
class BoundedSubprocessTests(unittest.TestCase):
def launch_fixture(self, text, argument, timeout=1):
real_popen = subprocess.Popen
children, calls = [], []
def fixture(argv, **kwargs):
calls.append((argv, kwargs))
child = real_popen(["/usr/bin/python3", "-B", "-c", text], **kwargs)
children.append(child)
return child
with patch.object(transport.subprocess, "Popen", side_effect=fixture):
try:
result = transport._run_rpc(argument, timeout)
return result, children, calls
finally:
self.assertTrue(all(child.poll() is not None for child in children))
def test_fixed_argv_inherits_normal_environment_without_shell(self):
argument = transport.validate_payload(make_payload())[1]
output = encode({"ok": True, "result": {"fixture": True}}) + b"\n"
result, children, calls = self.launch_fixture("import sys;sys.stdout.buffer.write(" + repr(output) + ")", argument)
self.assertEqual(result, (0, output, b""))
argv, kwargs = calls[0]
self.assertEqual(argv, ["/usr/bin/python3", "-B", "-m", "framebridge_expanded.real_screen.screen_rpc", argument])
self.assertNotIn("shell", kwargs)
self.assertNotIn("env", kwargs)
self.assertTrue(kwargs["start_new_session"])
self.assertTrue(kwargs["close_fds"])
self.assertEqual(len(children), 1)
def test_output_limit_does_not_accumulate_or_return_large_output(self):
for stream in ["stdout", "stderr"]:
with self.subTest(stream=stream):
with self.assertRaisesRegex(transport.ScreenProcessError, "SCREEN_RPC_OUTPUT_LIMIT"):
self.launch_fixture("import sys;sys." + stream + ".buffer.write(b'x'*100000);sys." + stream + ".flush()", "fixture")
def test_wall_timeout_kills_and_reaps_single_child(self):
started = time.monotonic()
with self.assertRaisesRegex(transport.ScreenProcessError, "SCREEN_RPC_TIMEOUT"):
self.launch_fixture("import time;time.sleep(5)", "fixture", timeout=1)
self.assertLess(time.monotonic() - started, 3)
def test_timeout_cleans_owned_group_after_leader_exits(self):
import signal
import tempfile
real_popen = subprocess.Popen
children = []
with tempfile.TemporaryDirectory(prefix="framebridge-screen-test-") as folder:
pidfile = Path(folder) / "descendant.pid"
script = ("import os,time\npid=os.fork()\n"
"if pid==0:\n time.sleep(5)\n os._exit(0)\n"
"open(" + repr(str(pidfile)) + ",'w').write(str(pid))\nos._exit(0)\n")
def launch(argv, **kwargs):
child = real_popen(["/usr/bin/python3", "-B", "-c", script], **kwargs)
children.append(child)
return child
try:
with patch.object(transport.subprocess, "Popen", side_effect=launch):
with self.assertRaisesRegex(transport.ScreenProcessError, "SCREEN_RPC_TIMEOUT"):
transport._run_rpc("fixture", 1)
descendant = int(pidfile.read_text())
def state():
try:
return (Path("/proc") / str(descendant) / "stat").read_text().rsplit(")", 1)[1].split()[0]
except FileNotFoundError:
return None
deadline = time.monotonic() + 0.5
while state() not in (None, "Z") and time.monotonic() < deadline:
time.sleep(0.01)
self.assertIn(state(), (None, "Z"), "owned descendant survived the RPC timeout")
finally:
# Keep the deliberately failing pre-fix regression free of live orphans.
for child in children:
try:
os.killpg(child.pid, signal.SIGKILL)
except ProcessLookupError:
pass
child.wait(timeout=1)
class TypedFleetRouteTests(unittest.TestCase):
def local_node(self):
return {"transport": "local", "alias": "central", "node_id": NODE_ID}
def remote_node(self):
return {"transport": "broker_outbound", "alias": "worker-1", "node_id": NODE_ID,
"operational_transport_ready": True}
def execute(self, payload):
return fleet.execute_screen_node("worker-1", payload["script"], True, payload["timeout_s"],
payload["job_id"], payload["sha256"], True)
def test_local_uses_typed_adapter_and_keeps_job_hash_uuid(self):
payload = make_payload({"op": "status"})
def adapter(value):
self.assertEqual(value, payload)
return response(value)
with patch.object(fleet, "resolve_node", return_value=self.local_node()), \
patch("framebridge_expanded.runtime.adapter_request", side_effect=adapter) as call:
result = self.execute(payload)
self.assertTrue(result["ok"])
self.assertEqual(result["target"], "LOCAL")
self.assertEqual(result["node_id"], NODE_ID)
self.assertEqual(result["mcp_job_id"], payload["job_id"])
self.assertEqual(result["mcp_script_sha256"], payload["sha256"])
call.assert_called_once()
def test_remote_admin_exec_preserves_type_through_broker_node(self):
payload = make_payload({"op": "status"})
calls = []
def adapter(value):
self.assertEqual(value, payload)
return response(value)
def request(node_id, capability, value, timeout_s):
calls.append((node_id, capability, value, timeout_s))
req = {"node_id": node_id, "expires_at": time.time() + 30,
"request_id": "request-fixture", "operation": capability, "payload": value}
with patch.object(node, "_context", return_value=(None, None, None, {"node_id": NODE_ID})):
return node.dispatch_request(req, adapter_request=adapter)
with patch.object(fleet, "resolve_node", return_value=self.remote_node()), \
patch.object(broker, "request", side_effect=request):
result = self.execute(payload)
self.assertTrue(result["ok"])
self.assertEqual(result["node_id"], NODE_ID)
self.assertEqual(result["target"], "worker-1")
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0][1], "admin.exec")
self.assertEqual(calls[0][2]["request_type"], "screen_rpc")
def test_old_adapter_is_reported_without_shell_retry(self):
remote = self.remote_node()
legacy = {"ok": False, "adapter_error": "ValueError:UNSUPPORTED_REQUEST_TYPE"}
def execute(**kwargs):
target = kwargs.pop("target")
return fleet.execute_screen_node(target, **kwargs)
client = ScreenClient(execute, image_factory=lambda **kwargs: None)
with patch.object(fleet, "resolve_node", return_value=remote), \
patch.object(broker, "request", return_value=legacy) as request, \
patch.object(fleet, "execute_node", side_effect=AssertionError("no legacy fallback")) as generic:
with self.assertRaisesRegex(ScreenError, "SCREEN_RPC_UNSUPPORTED_ON_NODE"):
client.remote("worker-1", {"op": "status"})
request.assert_called_once()
generic.assert_not_called()
def test_bad_job_or_hash_response_is_rejected(self):
payload = make_payload()
for field in ["job_id", "command_sha256"]:
with self.subTest(field=field):
result = response(payload)
result[field] = "invalid"
with patch.object(fleet, "resolve_node", return_value=self.remote_node()), \
patch.object(broker, "request", return_value=result):
final = self.execute(payload)
self.assertFalse(final["ok"])
self.assertIn("BINDING_INVALID", final["error"])
def test_offline_or_malformed_typed_payload_never_routes(self):
payload = make_payload()
for malformed in [dict(payload, script="echo not a screen RPC\n"),
dict(payload, sha256="0" * 64)]:
with self.subTest(malformed=malformed), patch.object(fleet, "resolve_node") as resolve:
result = self.execute(malformed)
self.assertFalse(result["ok"])
resolve.assert_not_called()
with patch.object(fleet, "resolve_node", return_value=dict(self.remote_node(), operational_transport_ready=False)), \
patch.object(broker, "request") as request:
result = self.execute(payload)
self.assertEqual(result["error"], "NODE_OFFLINE")
request.assert_not_called()
def test_generic_execute_remains_without_typed_marker(self):
payload = make_payload({"op": "status"})
calls = []
def adapter(value):
calls.append(value)
self.assertNotIn("request_type", value)
return response(value)
with patch.object(fleet, "resolve_node", return_value=self.local_node()), \
patch("framebridge_expanded.runtime.adapter_request", side_effect=adapter):
result = fleet.execute_node("LOCAL", payload["script"], True, 1, payload["job_id"], payload["sha256"], True)
self.assertTrue(result["ok"])
self.assertEqual(len(calls), 1)
if __name__ == "__main__":
unittest.main()
+85
View File
@@ -0,0 +1,85 @@
"""Run the actual viewer JS under GJS with a small DOM fixture.
This tests JS state and integrity handling, not browser decoding, CSP or ChatGPT UI.
"""
import base64
import json
import shutil
import subprocess
import unittest
from framebridge_expanded.real_screen.screen_presentation import VIEWER_HTML
from test_screen_frontdoor import png
@unittest.skipUnless(shutil.which("gjs"), "GJS is not installed on this test host")
class ScreenViewerJavaScriptTests(unittest.TestCase):
def test_original_bytes_recovery_errors_and_inflight_teardown(self):
data = png()
import hashlib
payload = {
"structuredContent": {
"ok": True, "node": "LOCAL", "hostname": "test-fixture",
"result": {
"capture_id": "1" * 32, "sha256": hashlib.sha256(data).hexdigest(),
"bytes": len(data), "width": 100, "height": 40,
"captured_at": "2026-10-24T12:00:00Z",
},
},
"_meta": {"framebridgeImage": {"mimeType": "image/png", "data": base64.b64encode(data).decode()}},
}
script = VIEWER_HTML.split("<script>", 1)[1].split("</script>", 1)[0]
bootstrap = r"""
const GLib=imports.gi.GLib;
const listeners={},elements={};
class Element {
constructor(){this.hidden=true;this.disabled=true;this.textContent="";this.attrs={};const classes=new Set();this.classList={add:x=>classes.add(x),remove:x=>classes.delete(x),contains:x=>classes.has(x)};}
setAttribute(k,v){this.attrs[k]=v;} removeAttribute(k){delete this.attrs[k];if(k==="src"||k==="href")this[k]="";}
async decode(){const b=GLib.base64_decode(this.src.split(",")[1]);const d=new DataView(b.buffer,b.byteOffset,b.byteLength);this.naturalWidth=d.getUint32(16);this.naturalHeight=d.getUint32(20);}
}
const fakeDocument={getElementById:id=>elements[id]||(elements[id]=new Element()),body:{scrollHeight:640}};
const fakeWindow={parent:{postMessage:()=>{}},addEventListener:(name,fn)=>{listeners[name]=fn;}};
globalThis.setTimeout=()=>0;
globalThis.atob=value=>Array.from(GLib.base64_decode(value),x=>String.fromCharCode(x)).join("");
async function realDigest(name,bytes){if(name!=="SHA-256")throw Error("wrong algorithm");const hex=GLib.compute_checksum_for_data(GLib.ChecksumType.SHA256,bytes);return Uint8Array.from(hex.match(/../g),x=>parseInt(x,16)).buffer;}
globalThis.crypto={subtle:{digest:realDigest}};
let checks=0;
function check(v,m){if(!v)throw Error(m);checks++;}
"""
cases = r"""
return (async()=>{
const fixture=PAYLOAD,original="data:image/png;base64,"+fixture._meta.framebridgeImage.data;
await render(fixture);
check(img.src===original,"original bytes changed");check(!img.hidden&&!viewport.hidden,"valid image hidden");
check(statusEl.textContent.includes("Original PNG verified"),"integrity success missing");
check(details.textContent.includes(fixture.structuredContent.result.sha256),"hash not displayed");
check(download.href===original,"download bytes changed");
native.onclick();check(viewport.classList.contains("native"),"native size unavailable");
fit.onclick();check(!viewport.classList.contains("native"),"fit unavailable");
await render({structuredContent:{ok:false,error:"TEST_ERROR"}});
check(img.hidden&&viewport.hidden,"error did not hide image");check(active===null,"error retained active capture");
check(!img.src&&!download.href&&!details.textContent,"error retained stale image data");
await render(fixture);check(!img.hidden&&img.src===original,"same original did not recover after error");
const bad=JSON.parse(JSON.stringify(fixture));bad.structuredContent.result.sha256="0".repeat(64);
await render(bad);check(img.hidden&&statusEl.textContent.includes("SHA-256 mismatch"),"corrupt digest not rejected");
await render(fixture);check(!img.hidden,"valid image did not recover after bad digest");
const slow=JSON.parse(JSON.stringify(fixture));slow.structuredContent.result.capture_id="2".repeat(32);
let finish;crypto.subtle.digest=(...args)=>new Promise(resolve=>{finish=()=>realDigest(...args).then(resolve);});
const inflight=render(slow);check(typeof finish==="function","digest did not start");
listeners.message({source:window.parent,data:{jsonrpc:"2.0",method:"ui/notifications/teardown",params:{}}});
await finish();await inflight;
check(img.hidden&&viewport.hidden&&!img.src,"inflight render revived after teardown");
check(fit.disabled&&native.disabled&&full.disabled,"teardown left controls enabled");
return checks;
})();
"""
executable = bootstrap + "\nconst run = function(window,document,crypto,atob,setTimeout){" + script + cases.replace("PAYLOAD", json.dumps(payload)) + "};\n"
executable += 'const loop=new GLib.MainLoop(null,false);let failed=false;run(fakeWindow,fakeDocument,globalThis.crypto,globalThis.atob,globalThis.setTimeout).then(n=>{print("PASS "+n+" viewer state assertions");loop.quit();}).catch(e=>{failed=true;printerr(String(e.stack||e));loop.quit();});loop.run();if(failed)imports.system.exit(1);'
result = subprocess.run([shutil.which("gjs"), "-c", executable],
capture_output=True, text=True, timeout=8)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("PASS 16 viewer state assertions", result.stdout)
if __name__ == "__main__":
unittest.main()
+141
View File
@@ -0,0 +1,141 @@
"""Real TLS and certificate-bootstrap checks in isolated application profiles."""
import hashlib
import ipaddress
import os
from pathlib import Path
import socket
import ssl
import stat
import tempfile
import threading
import unittest
from unittest import mock
from framebridge_expanded import broker, node, tls
from framebridge_expanded.config import default_config, load_config, save_config
ROOT = Path(__file__).resolve().parents[1]
class CertificateTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="tls-test-", dir=ROOT / "qa")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
def profile(self, name, role="central"):
state = self.root / name
save_config(state, default_config(role, name))
return state
def test_certificate_sans_fingerprint_private_modes_and_config(self):
state = self.profile("cert-central")
result = tls.create_certificates(state, "central.example.test", "192.0.2.10")
cfg = load_config(state)
x509, hashes, serialization, *_ = tls._crypto()
ca = x509.load_pem_x509_certificate(Path(result["ca_public_file"]).read_bytes())
cert = x509.load_pem_x509_certificate(Path(result["server_certificate_file"]).read_bytes())
self.assertEqual(result["ca_fingerprint_sha256"], ca.fingerprint(hashes.SHA256()).hex())
self.assertEqual(result["server_fingerprint_sha256"], cert.fingerprint(hashes.SHA256()).hex())
san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
self.assertEqual(set(san.get_values_for_type(x509.DNSName)), {"central.example.test", "localhost"})
self.assertEqual(set(san.get_values_for_type(x509.IPAddress)), {
ipaddress.ip_address("192.0.2.10"), ipaddress.ip_address("127.0.0.1"), ipaddress.ip_address("::1")})
self.assertTrue(ca.extensions.get_extension_for_class(x509.BasicConstraints).value.ca)
self.assertFalse(cert.extensions.get_extension_for_class(x509.BasicConstraints).value.ca)
self.assertEqual(stat.S_IMODE((state / "tls").stat().st_mode), 0o700)
for path in (state / "tls").iterdir():
expected = 0o644 if path.name == "ca-public.pem" else 0o600
self.assertEqual(stat.S_IMODE(path.stat().st_mode), expected, path.name)
self.assertEqual(cfg["tls_ca"], result["ca_public_file"])
self.assertEqual(cfg["broker_url"], "https://192.0.2.10:20878")
self.assertEqual(cfg["broker_control_url"], "https://127.0.0.1:20878")
self.assertEqual(cfg["broker_listen_host"], "127.0.0.1")
self.assertFalse(result["private_key_values_exported"])
self.assertFalse(result["openai_credentials_created_or_saved"])
self.assertFalse(any("private.key" in key for key in result))
def test_existing_material_is_never_overwritten(self):
state = self.profile("no-overwrite")
tls.create_certificates(state, "central.test", "127.0.0.1")
before = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in (state / "tls").iterdir()}
with self.assertRaises(FileExistsError):
tls.create_certificates(state, "replacement.test", "127.0.0.1")
after = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in (state / "tls").iterdir()}
self.assertEqual(before, after)
def test_invalid_or_active_profile_never_creates_material(self):
standalone = self.profile("standalone", "standalone")
with self.assertRaises(ValueError):
tls.create_certificates(standalone, "central.test", "127.0.0.1")
state = self.profile("active")
with mock.patch("framebridge_expanded.runtime.status", return_value={"running": True}):
with self.assertRaises(RuntimeError):
tls.create_certificates(state, "central.test", "127.0.0.1")
with self.assertRaises(ValueError):
tls.create_certificates(state, "bad host", "127.0.0.1")
with self.assertRaises(ValueError):
tls.create_certificates(state, "central.test", "0.0.0.0")
self.assertFalse((state / "tls").exists())
def test_real_https_node_enrollment_and_wrong_ca_rejection(self):
# Override only with a local, assigned address to exercise an actual LAN route.
address = os.environ.get("FRAMEBRIDGE_TLS_TEST_ADDRESS", "127.0.0.1")
ipaddress.ip_address(address)
with socket.socket() as probe:
probe.bind(("0.0.0.0", 0))
port = probe.getsockname()[1]
central = self.profile("https-central")
cfg = load_config(central)
cfg["ports"]["broker"] = port
if port in (cfg["ports"]["mcp"], cfg["ports"]["hub"]):
self.skipTest("Ephemeral broker port overlaps a default service port.")
save_config(central, cfg)
public = tls.create_certificates(central, "central.test", address, listen_host="0.0.0.0")
server = broker.make_server("0.0.0.0", port, central)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
joining = self.profile("https-node", "node")
nc = load_config(joining)
nc.update(broker_url=public["broker_url"], tls_ca=public["ca_public_file"])
save_config(joining, nc)
enrolled = node.enroll(public["broker_url"], "https-node", joining)
broker.configure(state_dir=central)
pending = broker.list_pending()
entry = next(p for p in pending if p["node_id"] == enrolled["node_id"])
self.assertEqual(entry["fingerprint"], enrolled["fingerprint"])
approved = broker.approve(entry["request_id"], enrolled["node_id"], central)
polled = node.poll_enrollment(joining)
self.assertEqual(polled["state"], "paired")
self.assertFalse(polled.get("broker_confirm_required", False))
self.assertTrue(any(p["node_id"] == enrolled["node_id"] for p in broker.list_confirmed_nodes()))
wrong = self.profile("wrong-authority")
wrong_public = tls.create_certificates(wrong, "other.test", address)
rejected = self.profile("wrong-ca-node", "node")
rc = load_config(rejected)
rc.update(broker_url=public["broker_url"], tls_ca=wrong_public["ca_public_file"])
save_config(rejected, rc)
before = broker.list_pending()
with self.assertRaises(Exception) as failure:
node.enroll(public["broker_url"], "wrong-ca-node", rejected)
# Check the verification error, rather than accepting any transport failure.
chain = []
error = failure.exception
for _ in range(6):
if error is None:
break
chain.append(str(error))
error = error.__cause__ or error.__context__
self.assertIn("CERTIFICATE_VERIFY_FAILED", " ".join(chain))
self.assertEqual(before, broker.list_pending())
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
self.assertFalse(thread.is_alive())
if __name__ == "__main__":
unittest.main()
+297
View File
@@ -0,0 +1,297 @@
"""Real MCP transport tests with real local Bash consoles and a bounded executor boundary.
The TEST_NODE executor runs on this QA host in another private profile; this
tests remote RPC framing, not a second machine. Actual multi-node VM coverage is separate.
"""
import asyncio
import json
import os
from pathlib import Path
import shlex
import socket
import subprocess
import sys
import tempfile
import time
import unittest
WORK = Path(__file__).resolve().parents[1]
VENDOR = WORK / "sources/original-geranas/FrameBridge-Fleet-Hub/vendor"
sys.path.insert(0, str(WORK / "expanded"))
sys.path.insert(0, str(VENDOR))
os.environ["PYTHONPATH"] = str(WORK / "expanded") + os.pathsep + str(VENDOR)
os.environ["FRAMEBRIDGE_EXPANDED_PREFIX"] = str(WORK)
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from framebridge_expanded import tty
from framebridge_expanded.tty_mcp import TOOL_NAMES, _remote_request, register_tools
from unittest import mock
from framebridge_expanded.real_screen.screen_frontdoor import ScreenClient
SERVER = r'''
from pathlib import Path
import json,os,subprocess
from mcp.server.fastmcp import FastMCP
from framebridge_expanded.tty_mcp import register_tools
port=int(os.environ["QA_TTY_MCP_PORT"])
mcp=FastMCP("Real CUA-TTY QA",host="127.0.0.1",port=port,streamable_http_path="/mcp")
def execute(script,authorized,timeout_s,job_id,sha256,include_stdout,target):
if authorized is not True or target!="TEST_NODE":
return {"ok":False,"error":"QA_TARGET_INVALID"}
env=os.environ.copy()
env["FRAMEBRIDGE_EXPANDED_STATE"]=env["QA_TTY_REMOTE_STATE"]
p=subprocess.run(["/bin/bash","--noprofile","--norc","-c",script],env=env,
stdin=subprocess.DEVNULL,capture_output=True,text=True,timeout=timeout_s)
with open(os.environ["QA_TTY_CALL_LOG"],"a") as log:
log.write(json.dumps({"read":" framebridge_expanded.tty read " in (" "+script+" "),
"stdout_bytes":len(p.stdout.encode())})+"\n")
return {"ok":True,"stdout":p.stdout[:4096],"stdout_truncated":len(p.stdout)>4096,
"exit_code":str(p.returncode)}
class ScreenFixture:
def call(self, op, target, authorized, **arguments):
return {"ok":True,"test_fixture":"screen-delegation-only",
"op":op,"target":target,"arguments":arguments}
register_tools(mcp,execute,screen_client=ScreenFixture())
mcp.run(transport="streamable-http")
'''
class RealMcpTtyTests(unittest.IsolatedAsyncioTestCase):
@classmethod
def setUpClass(cls):
cls.root = Path(tempfile.mkdtemp(prefix="tty-mcp-", dir=WORK / "qa"))
cls.local = cls.root / "local"
cls.remote = cls.root / "remote"
cls.local.mkdir(mode=0o700)
cls.remote.mkdir(mode=0o700)
assert tty.start(cls.local).get("ok")
assert tty.start(cls.remote).get("ok")
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
cls.port = sock.getsockname()[1]
child = cls.root / "server.py"
child.write_text(SERVER)
env = os.environ.copy()
env["PYTHONPATH"] = str(WORK / "expanded") + os.pathsep + str(VENDOR)
env.pop("DISPLAY", None)
env.pop("XDG_SESSION_TYPE", None)
env["FRAMEBRIDGE_EXPANDED_STATE"] = str(cls.local)
env["QA_TTY_REMOTE_STATE"] = str(cls.remote)
env["QA_TTY_MCP_PORT"] = str(cls.port)
cls.call_log = cls.root / "executor-calls.jsonl"
env["QA_TTY_CALL_LOG"] = str(cls.call_log)
log = (cls.root / "mcp-server.log").open("ab")
cls.server = subprocess.Popen([sys.executable, str(child)], env=env,
stdin=subprocess.DEVNULL, stdout=log,
stderr=subprocess.STDOUT)
log.close()
ready = False
deadline = time.monotonic() + 8
while cls.server.poll() is None and time.monotonic() < deadline:
with socket.socket() as sock:
if sock.connect_ex(("127.0.0.1", cls.port)) == 0:
ready = True
break
time.sleep(0.05)
if not ready:
tty.stop(cls.local)
tty.stop(cls.remote)
cls.server.wait(timeout=2)
raise AssertionError("Private MCP server failed to start")
@classmethod
def tearDownClass(cls):
cls.server.terminate()
try:
cls.server.wait(timeout=3)
except subprocess.TimeoutExpired:
cls.server.kill()
cls.server.wait(timeout=2)
assert tty.stop(cls.local).get("ok")
assert tty.stop(cls.remote).get("ok")
async def call(self, session, name, args):
result = await session.call_tool(name, args)
self.assertFalse(result.isError)
value = getattr(result, "structuredContent", None)
if not isinstance(value, dict):
value = next(json.loads(c.text) for c in result.content if c.type == "text")
return value
def remote_request(self, payload):
def execute(script, authorized, timeout_s, job_id, sha256, include_stdout, target):
if authorized is not True or target != "TEST_NODE":
return {"ok": False, "error": "QA_TARGET_INVALID"}
env = os.environ.copy()
env["FRAMEBRIDGE_EXPANDED_STATE"] = str(self.remote)
process = subprocess.run(["/bin/bash", "--noprofile", "--norc", "-c", script],
env=env, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=timeout_s)
with self.call_log.open("a") as log:
log.write(json.dumps({"read": " framebridge_expanded.tty read " in (" " + script + " "),
"stdout_bytes": len(process.stdout.encode())}) + "\n")
return {"ok": True, "stdout": process.stdout[:4096],
"stdout_truncated": len(process.stdout) > 4096, "exit_code": str(process.returncode)}
return _remote_request(execute, "TEST_NODE", payload)
async def test_lifecycle_over_mcp_and_persistent_bash_over_raw_rpc(self):
async with streamablehttp_client("http://127.0.0.1:" + str(self.port) + "/mcp") as streams:
async with ClientSession(streams[0], streams[1]) as session:
await session.initialize()
listing = await session.list_tools()
self.assertEqual(sorted(t.name for t in listing.tools), sorted(TOOL_NAMES))
denied = await self.call(session, "cua_tty_open", {"authorized": False})
self.assertEqual(denied["error"], "AUTHORIZED_TRUE_REQUIRED")
status = await self.call(session, "cua_tty_status", {"authorized": True})
self.assertTrue(status["ok"])
opened = await self.call(session, "cua_tty_open", {"authorized": True})
self.assertTrue(opened["ok"])
sid = opened["result"]["session_id"]
try:
marker = self.local / "mcp-local-marker.txt"
command = "printf local_mcp_real > " + shlex.quote(str(marker))
typed = tty.request(self.local, {"op": "text.type", "authorized": True,
"session_id": sid, "text": command})
self.assertTrue(typed["ok"])
keyed = tty.request(self.local, {"op": "key.send", "authorized": True,
"session_id": sid, "key": "ENTER"})
self.assertTrue(keyed["ok"])
deadline = time.monotonic() + 3
while not marker.exists() and time.monotonic() < deadline:
await asyncio.sleep(0.05)
self.assertEqual(marker.read_text(), "local_mcp_real")
resized = await self.call(session, "cua_tty_resize",
{"authorized": True, "session_id": sid, "rows": 33, "cols": 120})
self.assertTrue(resized["ok"])
observed = await self.call(session, "cua_tty_observe",
{"authorized": True, "session_id": sid})
self.assertEqual(observed["test_fixture"], "screen-delegation-only")
self.assertEqual(observed["op"], "observe")
self.assertEqual(observed["arguments"]["session_id"], sid)
transcript = tty.request(self.local, {"op": "observe", "authorized": True, "session_id": sid})
self.assertTrue(transcript["result"]["real_terminal"])
self.assertEqual(transcript["result"]["screen"]["rows"], 33)
finally:
closed = await self.call(session, "cua_tty_close",
{"authorized": True, "session_id": sid})
self.assertTrue(closed["result"]["cleanup_verified"])
async def test_remote_executor_framing_preserves_screen_larger_than_stdout_limit(self):
async with streamablehttp_client("http://127.0.0.1:" + str(self.port) + "/mcp") as streams:
async with ClientSession(streams[0], streams[1]) as session:
await session.initialize()
opened = await self.call(session, "cua_tty_open",
{"authorized": True, "target": "TEST_NODE", "rows": 32, "cols": 160})
self.assertTrue(opened["ok"], opened)
sid = opened["result"]["session_id"]
try:
marker = self.remote / "random-screen-done.txt"
code = ("import random,string; from pathlib import Path; r=random.Random(23); "
"[print(''.join(r.choice(string.ascii_letters+string.digits) for _ in range(150))) "
"for _ in range(28)]; Path(" + repr(str(marker)) + ").write_text('done')")
command = "python3 -c " + shlex.quote(code)
self.assertTrue(self.remote_request(
{"op": "text.type", "authorized": True, "session_id": sid, "text": command})["ok"])
self.assertTrue(self.remote_request(
{"op": "key.send", "authorized": True, "session_id": sid, "key": "ENTER"})["ok"])
deadline = time.monotonic() + 3
while not marker.exists() and time.monotonic() < deadline:
await asyncio.sleep(0.05)
self.assertEqual(marker.read_text(), "done")
# Raw observe is a terminal text transcript. Physical MCP observe
# is separately delegated to ScreenClient and never used here.
value = self.remote_request({"op": "observe", "authorized": True, "session_id": sid})
self.assertTrue(value["ok"], value)
self.assertTrue(value["result"]["real_terminal"])
self.assertGreater(len(json.dumps(value)), 4096)
records = [json.loads(line) for line in self.call_log.read_text().splitlines()]
self.assertTrue(any(r["read"] for r in records))
self.assertTrue(all(r["stdout_bytes"] < 4096 for r in records))
finally:
closed = await self.call(session, "cua_tty_close",
{"authorized": True, "target": "TEST_NODE", "session_id": sid})
self.assertTrue(closed["result"]["cleanup_verified"])
class FixtureMcp:
def __init__(self):
self.tools = {}
def tool(self, **_kwargs):
def register(function):
self.tools[function.__name__] = function
return function
return register
class McpScreenBindingTests(unittest.TestCase):
SID = "a" * 32
CAPTURE = "b" * 32
def tools(self, error="SESSION_NOT_VISIBLE", *, wrong_target=False):
calls = []
def execute(**kwargs):
calls.append(kwargs)
return {"ok": True, "exit_code": "0", "stdout_truncated": False,
"target": "different-node" if wrong_target else kwargs["target"],
"node_id": "12345678-1234-1234-1234-123456789abc",
"mcp_job_id": kwargs["job_id"], "mcp_script_sha256": kwargs["sha256"],
"stdout": json.dumps({"ok": False, "error": error})}
client = ScreenClient(execute, image_factory=lambda **kwargs: kwargs, resolve_node=lambda node: node)
mcp = FixtureMcp()
register_tools(mcp, execute, screen_client=client)
return mcp.tools, calls
def test_seven_existing_names_remain_registered(self):
tools, _calls = self.tools()
self.assertEqual(set(tools), set(TOOL_NAMES))
def test_missing_capture_rejected_before_any_backend_execution(self):
tools, calls = self.tools()
with mock.patch.object(tty, "request") as raw:
value = tools["cua_tty_key_send"](self.SID, "ENTER", True, "NodeCase")
self.assertEqual(value["error"], "SCREEN_CAPTURE_ID_REQUIRED")
self.assertEqual(calls, [])
raw.assert_not_called()
def test_unapproved_screen_observation_does_not_reach_backend(self):
tools, calls = self.tools()
value = tools["cua_tty_observe"](self.SID, False, "NodeCase")
self.assertEqual(value["error"], "AUTHORIZED_TRUE_REQUIRED")
self.assertEqual(calls, [])
def test_hidden_session_error_has_no_raw_pty_fallback(self):
tools, calls = self.tools()
with mock.patch.object(tty, "request") as raw:
value = tools["cua_tty_text_type"](self.SID, "not executed", True, "NodeCase", self.CAPTURE)
self.assertEqual(value["error"], "SESSION_NOT_VISIBLE")
self.assertEqual(calls[0]["target"], "NodeCase")
raw.assert_not_called()
def test_mismatched_executor_target_cannot_satisfy_screen_request(self):
tools, _calls = self.tools(wrong_target=True)
value = tools["cua_tty_key_send"](self.SID, "ENTER", True, "NodeCase", self.CAPTURE)
self.assertEqual(value["error"], "SCREEN_EXEC_BINDING_INVALID")
def test_graphical_input_rejection_is_preserved_without_hidden_fallback(self):
tools, _calls = self.tools(error="USE_GRAPHICAL_CUA")
with mock.patch.object(tty, "request") as raw:
value = tools["cua_tty_key_send"](self.SID, "ENTER", True, "NodeCase", self.CAPTURE)
self.assertEqual(value["error"], "USE_GRAPHICAL_CUA")
raw.assert_not_called()
def test_observe_delegation_preserves_requested_session_and_node(self):
record = []
class ScreenFixture:
def call(self, *args, **kwargs):
record.append((args, kwargs))
return {"test_fixture": "delegation-only"}
mcp = FixtureMcp()
register_tools(mcp, lambda **_kw: self.fail("raw execute is not the observe path"),
screen_client=ScreenFixture())
value = mcp.tools["cua_tty_observe"](self.SID, True, "NodeCase")
self.assertEqual(value, {"test_fixture": "delegation-only"})
self.assertEqual(record, [(("observe", "NodeCase", True), {"session_id": self.SID})])
if __name__ == "__main__":
unittest.main()
+151
View File
@@ -0,0 +1,151 @@
"""Real headless Bash/tmux integration tests; only test-owned profiles are closed."""
import json
import os
from pathlib import Path
import shlex
import stat
import sys
import tempfile
import time
import unittest
WORK = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(WORK / "expanded"))
os.environ["PYTHONPATH"] = str(WORK / "expanded")
os.environ["FRAMEBRIDGE_EXPANDED_PREFIX"] = str(WORK)
from framebridge_expanded import tty
class RealTtyTests(unittest.TestCase):
def setUp(self):
self.state = Path(tempfile.mkdtemp(prefix="tty-test-", dir=WORK / "qa"))
self.sessions = []
self.previous_display = os.environ.pop("DISPLAY", None)
self.previous_x11 = os.environ.pop("XDG_SESSION_TYPE", None)
self.assertTrue(tty.start(self.state).get("ok"))
def tearDown(self):
for sid in self.sessions:
tty.request(self.state, {"op": "close", "authorized": True, "session_id": sid})
self.assertTrue(tty.stop(self.state).get("ok"))
self.assertFalse((tty.runtime_path(self.state) / "agent.sock").exists())
if self.previous_display is not None:
os.environ["DISPLAY"] = self.previous_display
if self.previous_x11 is not None:
os.environ["XDG_SESSION_TYPE"] = self.previous_x11
def call(self, op, **args):
return tty.request(self.state, {"op": op, "authorized": True, **args})
def open(self, **args):
value = self.call("open", **args)
self.assertTrue(value.get("ok"), value)
self.sessions.append(value["result"]["session_id"])
return value["result"]
def enter(self, sid, text):
self.assertTrue(self.call("text.type", session_id=sid, text=text).get("ok"))
self.assertTrue(self.call("key.send", session_id=sid, key="ENTER").get("ok"))
def await_file(self, path):
deadline = time.monotonic() + 3
while not path.exists() and time.monotonic() < deadline:
time.sleep(0.05)
self.assertTrue(path.is_file(), str(path))
return path.read_text()
def test_real_persistent_bash_and_unicode_without_x11(self):
self.assertNotIn("DISPLAY", os.environ)
opened = self.open(rows=30, cols=100)
self.assertTrue(opened["real_terminal"])
self.assertEqual(opened["backend"], "tmux")
self.assertTrue(Path(opened["tty"]).exists())
self.assertGreater(opened["pid"], 1)
sid = opened["session_id"]
directory = self.state / "working-directory"
directory.mkdir()
self.enter(sid, "export FRAMEBRIDGE_TTY_VALUE=" + shlex.quote("persistent ñ 漢字")
+ "; cd " + shlex.quote(str(directory)))
marker = self.state / "unicode-and-state.txt"
self.enter(sid, 'printf %s "$FRAMEBRIDGE_TTY_VALUE" > ' + shlex.quote(str(marker)))
self.assertEqual(self.await_file(marker), "persistent ñ 漢字")
observed = self.call("observe", session_id=sid)
self.assertTrue(observed["ok"])
self.assertEqual(observed["result"]["pid"], opened["pid"])
self.assertEqual(observed["result"]["cwd"], str(directory))
self.assertGreaterEqual(observed["result"]["sequence"], 4)
def test_control_c_interrupts_a_real_foreground_process(self):
sid = self.open()["session_id"]
self.enter(sid, "sleep 30")
deadline = time.monotonic() + 2
sleeping = False
while time.monotonic() < deadline:
value = self.call("observe", session_id=sid)
if value["result"]["current_command"] == "sleep":
sleeping = True
break
time.sleep(0.05)
self.assertTrue(sleeping)
self.assertTrue(self.call("key.send", session_id=sid, key="CTRL_C")["ok"])
marker = self.state / "interrupted.txt"
self.enter(sid, "printf interrupted > " + shlex.quote(str(marker)))
self.assertEqual(self.await_file(marker), "interrupted")
def test_real_resize_can_exceed_the_old_machine_specific_bounds(self):
sid = self.open()["session_id"]
self.assertTrue(self.call("resize", session_id=sid, rows=110, cols=240)["ok"])
observed = self.call("observe", session_id=sid)["result"]
self.assertEqual((observed["screen"]["rows"], observed["screen"]["cols"]), (110, 240))
def test_authorization_and_socket_identity_are_explicit(self):
before = self.call("status")["result"]["active_sessions"]
denied = tty.request(self.state, {"op": "open", "authorized": False})
self.assertEqual(denied.get("error"), "AUTHORIZED_TRUE_REQUIRED")
denied = tty.request(self.state, {"op": "open", "authorized": 1})
self.assertEqual(denied.get("error"), "AUTHORIZED_TRUE_REQUIRED")
self.assertEqual(self.call("status")["result"]["active_sessions"], before)
path = tty.runtime_path(self.state) / "agent.sock"
st = path.stat()
self.assertTrue(stat.S_ISSOCK(st.st_mode))
self.assertEqual(stat.S_IMODE(st.st_mode), 0o600)
self.assertEqual(st.st_uid, os.geteuid())
status = self.call("status")["result"]
self.assertEqual(status["input_policy"], "same-uid-authorized")
self.assertNotIn("owner_device_connected", status)
self.assertNotIn("owner_signature_required", status)
def test_errors_do_not_expose_arbitrary_payloads_or_paths(self):
self.assertEqual(self.call("open", program="demo-readonly").get("error"),
"PROGRAM_NOT_SUPPORTED")
self.assertEqual(self.call("observe", session_id="../../outside").get("error"),
"SESSION_ID_INVALID")
sid = self.open()["session_id"]
self.assertEqual(self.call("key.send", session_id=sid, key="INVALID_PRIVATE_TEXT").get("error"),
"KEY_NOT_ALLOWLISTED")
closed = self.call("close", session_id=sid)
self.assertTrue(closed["result"]["cleanup_verified"])
self.sessions.remove(sid)
self.assertEqual(self.call("observe", session_id=sid).get("error"), "SESSION_NOT_FOUND")
def test_configurable_session_limit_replaces_the_old_constructor_cap(self):
self.assertTrue(tty.stop(self.state)["ok"])
(self.state / "tty.json").write_text(json.dumps({"limits": {"max_sessions": 5}}))
self.assertTrue(tty.start(self.state)["ok"])
for _ in range(5):
self.open(rows=10, cols=40)
self.assertEqual(self.call("status")["result"]["active_sessions"], 5)
self.assertEqual(self.call("open").get("error"), "SESSION_LIMIT")
def test_configured_input_limit_is_reported_and_enforced(self):
self.assertTrue(tty.stop(self.state)["ok"])
(self.state / "tty.json").write_text(json.dumps({"limits": {"max_text_bytes": 100}}))
self.assertTrue(tty.start(self.state)["ok"])
sid = self.open()["session_id"]
self.assertEqual(self.call("status")["result"]["limits"]["max_text_bytes"], 100)
self.assertEqual(self.call("text.type", session_id=sid, text="x" * 101).get("error"),
"TEXT_INVALID_OR_TOO_LARGE")
if __name__ == "__main__":
unittest.main()
+372
View File
@@ -0,0 +1,372 @@
"""Owned transient-unit tests; never invokes sudo or changes system policy."""
import os
from pathlib import Path
import shutil
import sys
import tempfile
import time
import unittest
from unittest import mock
import uuid
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "expanded"))
os.environ["FRAMEBRIDGE_EXPANDED_PREFIX"] = str(ROOT)
from framebridge_expanded import tty_sudo_control as control
class SudoControlTests(unittest.TestCase):
def setUp(self):
runtime = Path("/run/user") / str(os.getuid())
self.temp = tempfile.TemporaryDirectory(prefix="fb-sudo-qa-", dir=runtime)
self.base = Path(self.temp.name)
self.controller = control.SudoConsoleController(self.base, timeout=5)
self.records = []
self.addCleanup(self.cleanup)
def cleanup(self):
for record in reversed(self.records):
try:
self.controller.close(record)
except control.SudoControlError as error:
if error.code not in {"TTY_SUDO_BACKEND_FAILED", "TTY_SUDO_PROCESS_NOT_FOUND",
"TTY_SUDO_CONSOLE_EXITED", "TTY_SUDO_UNIT_NOT_RUNNING"}:
raise
self.temp.cleanup()
def launch(self, **kwargs):
record = self.controller.launch(uuid.uuid4().hex, **kwargs)
self.records.append(record)
return record
def wait(self, record, predicate, seconds=2):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
value = self.controller.status(record)
if predicate(value):
return value
time.sleep(0.03)
self.fail("Expected controlled console state was not reached.")
def send(self, record, command):
self.controller._tmux(record, "send-keys", "-t", record["pane"], "-l", command)
self.controller._tmux(record, "send-keys", "-t", record["pane"], "Enter")
def test_roundtrip_real_nnp_and_recreation_preserves_cwd_size(self):
cwd = self.base / "directory"
cwd.mkdir()
off = self.launch(cwd=cwd, rows=31, cols=97)
self.assertEqual((off["no_new_privs"], off["server_no_new_privs"]), (1, 1))
self.assertTrue(off["prompt_verified"])
on = self.controller.set_mode(off, "on")
self.assertEqual((on["no_new_privs"], on["server_no_new_privs"]), (0, 0))
self.assertNotEqual(on["pid"], off["pid"])
self.assertEqual((on["cwd"], on["rows"], on["cols"]), (str(cwd), 31, 97))
self.assertIsNone(control._proc(off["pid"]))
blocked = self.controller.set_mode(on, "off")
self.assertEqual((blocked["no_new_privs"], blocked["server_no_new_privs"]), (1, 1))
self.assertNotEqual(blocked["pid"], on["pid"])
self.assertFalse(blocked["sudo_authorization_granted"])
self.assertEqual(blocked["sudo_authentication"], "system_policy")
def test_same_mode_is_idempotent_for_managed_console(self):
record = self.launch()
same = self.controller.set_mode(record, "off")
self.assertFalse(same["recreated"])
self.assertEqual(record["pid"], same["pid"])
def test_builtin_read_is_busy_without_a_child_process(self):
record = self.launch()
self.send(record, "read -r __fb_public_test")
busy = self.wait(record, lambda v: v["busy"] and not v["prompt_verified"])
self.assertFalse(busy["busy_pids"])
with self.assertRaises(control.SudoControlError) as error:
self.controller.set_mode(record, "on")
self.assertEqual(error.exception.code, "TTY_SUDO_PROMPT_UNVERIFIED")
self.controller._tmux(record, "send-keys", "-t", record["pane"], "C-c")
self.wait(record, lambda v: not v["busy"])
def test_background_job_blocks_transition(self):
record = self.launch()
self.send(record, "sleep 30 &")
self.wait(record, lambda v: bool(v["busy_pids"]))
with self.assertRaises(control.SudoControlError) as error:
self.controller.set_mode(record, "on")
self.assertEqual(error.exception.code, "TTY_SUDO_CONSOLE_BUSY")
def test_reparented_setsid_disowned_job_is_found_by_cgroup(self):
record = self.launch()
self.send(record, "setsid -f /bin/sleep 30 </dev/null >/dev/null 2>&1 & disown")
busy = self.wait(record, lambda v: bool(v["busy_pids"]))
processes = control._processes()
descendants = control._descendants(processes, record["pid"])
self.assertTrue(set(busy["busy_pids"]) - descendants)
with self.assertRaises(control.SudoControlError):
self.controller.set_mode(record, "on")
def test_stopped_job_blocks_transition(self):
record = self.launch()
self.send(record, "sleep 30")
self.wait(record, lambda v: bool(v["busy_pids"]))
self.controller._tmux(record, "send-keys", "-t", record["pane"], "C-z")
busy = self.wait(record, lambda v: any(
control._proc(p) and control._proc(p)["state"] == "T" for p in v["busy_pids"]))
with self.assertRaises(control.SudoControlError):
self.controller.set_mode(record, "on")
self.assertTrue(busy["busy"])
def test_candidate_is_rolled_back_if_original_becomes_busy(self):
record = self.launch()
original = self.controller._idle
calls = 0
def idle(value):
nonlocal calls
calls += 1
if calls == 2:
raise control.SudoControlError("TTY_SUDO_CONSOLE_BUSY")
return original(value)
with mock.patch.object(self.controller, "_idle", side_effect=idle):
with self.assertRaises(control.SudoControlError):
self.controller.set_mode(record, "on")
current = self.controller.status(record)
self.assertEqual((current["pid"], current["mode"]), (record["pid"], "off"))
self.assertEqual(len(list((self.base / "sudo").glob("*.sock"))), 1)
def test_postkill_cleanup_failure_keeps_verified_replacement(self):
record = self.launch()
original = self.controller._close
def close(value, require_idle=False):
result = original(value, require_idle=require_idle)
if value["pid"] == record["pid"]:
raise control.SudoControlError("TTY_SUDO_BACKEND_TIMEOUT")
return result
with mock.patch.object(self.controller, "_close", side_effect=close):
replacement = self.controller.set_mode(record, "on")
self.assertTrue(replacement["cleanup_pending"])
self.assertFalse(replacement["transition_complete"])
self.assertEqual(replacement["cleanup_error"], "TTY_SUDO_BACKEND_TIMEOUT")
self.assertIsNone(control._proc(record["pid"]))
self.assertIsNotNone(control._proc(replacement["pid"]))
self.assertEqual(replacement["no_new_privs"], 0)
with mock.patch.object(self.controller, "_old_cleanup_complete", return_value=False):
state = self.controller.status(record)
self.assertTrue(state["cleanup_pending"])
self.assertEqual(state["pid"], replacement["pid"])
with self.assertRaises(control.SudoControlError) as error:
self.controller.set_mode(record, "off")
self.assertEqual(error.exception.code, "TTY_SUDO_TRANSITION_CLEANUP_PENDING")
with self.assertRaises(control.SudoControlError):
self.controller.close(record)
state = self.controller.status(record)
self.assertFalse(state["cleanup_pending"])
self.assertTrue(state["transition_complete"])
self.assertNotIn("pending_old_record", state)
def test_legacy_status_does_not_fake_prompt_verification(self):
record = self.launch()
legacy = {key: record[key] for key in ("name", "pane")}
legacy.update(session_id=uuid.uuid4().hex, legacy_socket=record["tmux_socket"])
state = self.controller.status(legacy)
self.assertEqual(state["no_new_privs"], 1)
self.assertFalse(state["prompt_verified"])
with self.assertRaises(control.SudoControlError) as error:
self.controller.set_mode(legacy, "on")
self.assertEqual(error.exception.code, "TTY_SUDO_PROMPT_UNVERIFIED")
def test_only_own_unit_names_are_accepted(self):
with self.assertRaises(control.SudoControlError) as error:
self.controller._unit("unrelated-existing.service")
self.assertEqual(error.exception.code, "TTY_SUDO_UNIT_NOT_OWNED")
def test_pid_birth_change_fails_closed(self):
record = self.launch()
self.controller._records[record["session_id"]]["birth"] = "not-the-birth"
try:
with self.assertRaises(control.SudoControlError) as error:
self.controller.status(record)
self.assertEqual(error.exception.code, "TTY_SUDO_PROCESS_IDENTITY_CHANGED")
finally:
self.controller._records[record["session_id"]]["birth"] = record["birth"]
def test_elevated_process_metadata_refuses_change_and_close(self):
record = self.launch()
original = control._processes
def processes():
data = original()
data[987654321] = {"pid": 987654321, "ppid": record["pid"],
"sid": 987654321, "tty": 0, "uids": (0, 0, 0, 0)}
return data
with mock.patch.object(control, "_processes", side_effect=processes):
with self.assertRaises(control.SudoControlError) as error:
self.controller.set_mode(record, "on")
self.assertEqual(error.exception.code, "TTY_SUDO_ELEVATED_PROCESS_PRESENT")
with self.assertRaises(control.SudoControlError):
self.controller.close(record)
def test_marker_contains_only_generation_and_state(self):
record = self.launch()
self.send(record, "printf '%s\\n' PUBLIC_INPUT_MARKER")
self.wait(record, lambda v: not v["busy"])
value = Path(record["prompt_marker"]).read_text()
self.assertEqual(value, record["generation"] + ":idle\n")
rc = Path(record["rcfile"]).read_text()
self.assertNotIn("PUBLIC_INPUT_MARKER", rc)
self.assertNotIn("BASH_COMMAND", rc)
self.assertEqual(Path(record["rcfile"]).stat().st_mode & 0o777, 0o600)
def test_exit_still_cleans_up_owned_server_and_scope(self):
record = self.launch()
self.send(record, "exit")
deadline = time.monotonic() + 2
while time.monotonic() < deadline:
process = control._proc(record["pid"])
if process is None or process["state"] in {"Z", "X"}:
break
time.sleep(0.03)
result = self.controller.close(record)
self.assertTrue(result["closed"])
self.assertFalse(Path(record["tmux_socket"]).exists())
self.assertIsNone(control._proc(record["server_pid"]))
self.records.remove(record)
def test_invalid_mode_does_not_launch(self):
with self.assertRaises(control.SudoControlError):
self.controller.launch(uuid.uuid4().hex, mode="automatic-root")
self.assertFalse(list((self.base / "sudo").glob("*.sock")))
class PortableSudoLimitsTests(SudoControlTests):
# Reuse lifecycle helpers, but do not repeat the inherited test methods.
def test_expanded_large_geometry_survives_mode_change(self):
record = self.launch(rows=500, cols=1000)
self.assertEqual((record["rows"], record["cols"]), (500, 1000))
enabled = self.controller.set_mode(record, "on")
self.assertEqual((enabled["rows"], enabled["cols"], enabled["no_new_privs"]), (500, 1000, 0))
def test_profile_limits_reject_before_starting_a_unit(self):
controller = control.SudoConsoleController(
self.base / "profile", min_rows=3, max_rows=8, min_cols=11, max_cols=30)
with mock.patch.object(controller, "_run", side_effect=AssertionError("unit must not start")):
for rows, cols in [(2, 20), (9, 20), (4, 10), (4, 31), (True, 20)]:
with self.assertRaises(control.SudoControlError) as error:
controller.launch(uuid.uuid4().hex, rows=rows, cols=cols)
self.assertEqual(error.exception.code, "TTY_SUDO_SIZE_INVALID")
def test_actual_nnp_mismatch_rolls_back_candidate(self):
with mock.patch.object(self.controller, "_inspect",
return_value={"no_new_privs": 0, "busy": False}):
with self.assertRaises(control.SudoControlError) as error:
self.controller.launch(uuid.uuid4().hex, mode="off")
self.assertEqual(error.exception.code, "TTY_SUDO_PRIVILEGE_STATE_MISMATCH")
self.assertFalse(list((self.base / "sudo").glob("*.sock")))
self.assertFalse(self.controller._records)
def test_profile_runtime_isolation(self):
other = control.SudoConsoleController(self.base / "other")
self.assertNotEqual(other.base, self.controller.base)
self.assertNotEqual(other.prefix, self.controller.prefix)
def test_private_profile_bus_does_not_redirect_user_manager(self):
private = self.base / "private-runtime"
private.mkdir(mode=0o700)
with mock.patch.dict(os.environ, {
"XDG_RUNTIME_DIR": str(private),
"DBUS_SESSION_BUS_ADDRESS": "unix:path=" + str(private / "not-the-user-manager")}):
record = self.launch(mode="off")
self.assertEqual(record["no_new_privs"], 1)
self.assertEqual(record["server_no_new_privs"], 1)
self.assertEqual(self.controller.user_runtime, Path("/run/user") / str(os.getuid()))
def test_profile_graphical_context_is_explicitly_forwarded_to_unit(self):
import shlex
private = self.base / "profile-desktop"
private.mkdir(mode=0o700)
expected = {
"DISPLAY": ":193",
"XAUTHORITY": str(private / "authority-not-used"),
"DBUS_SESSION_BUS_ADDRESS": "unix:path=" + str(private / "private-bus-not-used"),
"XDG_RUNTIME_DIR": str(private),
"LD_LIBRARY_PATH": str(private / "libraries"),
"LIBGL_DRIVERS_PATH": str(private / "drivers"),
"GBM_BACKENDS_PATH": str(private / "gbm"),
"__EGL_VENDOR_LIBRARY_FILENAMES": str(private / "egl-not-used.json"),
"__GLX_VENDOR_LIBRARY_NAME": "mesa",
"GALLIUM_DRIVER": "llvmpipe",
"MESA_LOADER_DRIVER_OVERRIDE": "zink",
"VK_DRIVER_FILES": str(private / "vk-not-used.json"),
"VK_ICD_FILENAMES": str(private / "vk-compat-not-used.json"),
"DRI_PRIME": "0",
}
forbidden = "FRAMEBRIDGE_TEST_NOT_FORWARDED"
with mock.patch.dict(os.environ, dict(expected, **{forbidden: "MUST_NOT_FORWARD"})):
controller = control.SudoConsoleController(self.base / "graphical-controller")
record = controller.launch(uuid.uuid4().hex, mode="off")
self.addCleanup(controller.close, record)
self.assertEqual(record["no_new_privs"], 1)
self.assertEqual(record["server_no_new_privs"], 1)
self.assertEqual(controller.session_environment, expected)
result = controller._run([
controller.systemctl, "--user", "show", record["unit_name"],
"--property=Environment", "--value"])
unit_environment = dict(value.split("=", 1)
for value in shlex.split(result.stdout.decode()))
for name, value in expected.items():
self.assertEqual(unit_environment.get(name), value)
self.assertNotIn(forbidden, unit_environment)
self.assertEqual(controller.user_bus_address,
"unix:path=/run/user/" + str(os.getuid()) + "/bus")
# Do not run inherited acceptance cases twice.
for _name in tuple(name for name in dir(SudoControlTests) if name.startswith("test_")):
setattr(PortableSudoLimitsTests, _name, None)
class PortableSudoRpcTests(unittest.TestCase):
def setUp(self):
import threading
from framebridge_expanded import tty
self.tty = tty
class FakeManager:
_lock = threading.RLock()
def reap(self): pass
def touch(self, sid): self.last_touch = sid
def sudo(self, sid, mode):
return {"session_id": sid, "mode": mode, "no_new_privs": 1 if mode == "off" else 0}
self.manager = FakeManager()
self.sid = "a" * 32
self.payload = {"op": "sudo", "mode": "on", "session_id": self.sid, "authorized": True}
def test_authorized_typed_sudo_dispatch(self):
reply = self.tty.dispatch(self.manager, self.payload)
self.assertTrue(reply["ok"])
self.assertEqual(reply["result"]["session_id"], self.sid)
self.assertEqual(reply["result"]["mode"], "on")
self.assertEqual(self.manager.last_touch, self.sid)
def test_missing_authorization_and_extra_keys_are_rejected(self):
for payload in [dict(self.payload, authorized=False), dict(self.payload, command="sudo id")]:
with self.assertRaises(self.tty.TerminalDenied):
self.tty.dispatch(self.manager, payload)
def test_cleanup_pending_keeps_verified_replacement_in_failure_reply(self):
self.manager.sudo = lambda sid, mode: {"session_id": sid, "cleanup_pending": True, "mode": mode}
reply = self.tty.dispatch(self.manager, self.payload)
self.assertFalse(reply["ok"])
self.assertEqual(reply["error"], "TTY_SUDO_TRANSITION_CLEANUP_PENDING")
self.assertEqual(reply["result"]["session_id"], self.sid)
status = self.tty.dispatch(self.manager, dict(self.payload, mode="status"))
self.assertTrue(status["ok"])
def test_controller_error_is_public_fixed_code(self):
error = control.SudoControlError("TTY_SUDO_CONSOLE_BUSY")
self.assertEqual(self.tty._safe_error(error), "TTY_SUDO_CONSOLE_BUSY")
if __name__ == "__main__":
unittest.main()
+323
View File
@@ -0,0 +1,323 @@
import io
import os
from pathlib import Path
import signal
import sys
import time
import subprocess
import tempfile
import unittest
from unittest import mock
from framebridge_expanded import tunnel
ID = "tunnel_0123456789abcdef0123456789abcdef"
ROOT = Path(__file__).resolve().parents[1]
class ParserTests(unittest.TestCase):
def test_id_and_documented_commands(self):
commands = [
ID,
"'" + ID + "'",
"tunnel-client run --control-plane.tunnel-id " + ID,
"/opt/client/tunnel-client run --control-plane.tunnel-id=" + ID + " --control-plane.api-key env:OLD_KEY --mcp.server-url url=http://127.0.0.1:8898/mcp",
"tunnel-client init --profile demo --sample sample_mcp_remote_no_auth --tunnel-id " + ID + " --mcp-server-url http://127.0.0.1:8898/mcp",
"tunnel-client runtimes connect --alias demo --tunnel-id " + ID + " --runtime-api-key env:OLD_KEY --json",
"framebridge-original tunnel -- run --control-plane.tunnel-id " + ID,
]
for command in commands:
with self.subTest(command=command):
self.assertEqual(tunnel.parse_tunnel_input(command).tunnel_id, ID)
def test_rejections_never_echo_hidden_input(self):
secret = "sk-test-DO_NOT_DISCLOSE-0123456789"
bad = [
"sudo tunnel-client run --control-plane.tunnel-id " + ID,
"env OPENAI_API_KEY=" + secret + " tunnel-client run --control-plane.tunnel-id " + ID,
"tunnel-client run --control-plane.tunnel-id " + ID + "; echo bad",
"tunnel-client run --control-plane.tunnel-id $(" + secret + ")",
"tunnel-client run --control-plane.tunnel-id " + ID + " | cat",
"tunnel-client run --control-plane.tunnel-id " + ID + " --config /private/profile.yaml",
"tunnel-client run --control-plane.tunnel-id " + ID + " --control-plane.api-key " + secret,
"tunnel-client run --control-plane.tunnel-id " + ID + " --control-plane.api-key file:/private/key",
"tunnel-client run --control-plane.tunnel-id " + ID + " --control-plane.base-url https://example.invalid",
"tunnel-client run --control-plane.tunnel-id " + ID + " --tunnel-id " + ID,
"tunnel-client run --control-plane.tunnel-id",
"tunnel-client run --control-plane.tunnel-id '" + secret,
]
for command in bad:
with self.subTest(command=command):
with self.assertRaises(tunnel.TunnelInputError) as error:
tunnel.parse_tunnel_input(command)
self.assertNotIn(secret, str(error.exception))
self.assertNotIn(command, str(error.exception))
def test_rebuild_uses_packaged_binary_and_profile_endpoint(self):
spec = tunnel.parse_tunnel_input("tunnel-client run --control-plane.tunnel-id " + ID + " --mcp.server-url https://mcp.example.com/mcp")
args = tunnel.build_command("/packaged/tunnel-client", "/own/state", "http://127.0.0.1:20865/mcp", spec)
self.assertEqual(args[0], "/packaged/tunnel-client")
self.assertIn("url=http://127.0.0.1:20865/mcp", args)
self.assertNotIn("https://mcp.example.com/mcp", args)
self.assertIn("env:" + tunnel.KEY_ENV, args)
def test_inherited_runtime_settings_removed_without_global_mutation(self):
original = {"PATH": "/usr/bin", "DISPLAY": ":77", "HTTPS_PROXY": "http://proxy.example", "CA_BUNDLE": "/ca.pem",
"TUNNEL_CLIENT_CONFIG": "/production/config", "CONTROL_PLANE_TUNNEL_ID": "production",
"MCP_SERVER_URL": "https://production", "MCP_COMMAND": "production-command",
"HEALTH_URL_FILE": "/production/health", "LOG_HTTP_RAW_UNSAFE": "true",
"CLOUDFLARED_TUNNEL_TOKEN": "not-used", "OPENAI_API_KEY": "not-used"}
snapshot = dict(original)
env = tunnel.child_environment("FAKE_RUNTIME_KEY", original)
self.assertEqual(original, snapshot)
for key in snapshot:
if key.startswith(tunnel._RUNTIME_PREFIXES):
self.assertNotIn(key, env)
self.assertEqual(env["HTTPS_PROXY"], original["HTTPS_PROXY"])
self.assertEqual(env[tunnel.KEY_ENV], "FAKE_RUNTIME_KEY")
def test_child_default_profile_paths_are_private_and_scoped(self):
with tempfile.TemporaryDirectory(prefix="tunnel-test-", dir=ROOT / "qa") as td:
env = tunnel.child_environment("FAKE_ONLY", {"HOME": "/old/home", "XDG_CONFIG_HOME": "/old/config"}, state=Path(td))
self.assertEqual(env["HOME"], str(Path(td) / "chatgpt-home"))
self.assertEqual(env["XDG_CONFIG_HOME"], str(Path(td) / "chatgpt-home/.config"))
self.assertEqual(Path(env["TMPDIR"]).stat().st_mode & 0o777, 0o700)
self.assertFalse(any(p.is_file() for p in Path(td).rglob("*")))
def test_saved_public_context_is_reused(self):
public = {"tunnel_id": ID, "control_plane_organization_id": "org_test", "control_plane_url_path": "/v1/tunnel"}
with mock.patch.object(tunnel, "_hidden", side_effect=["", "FAKE_ONLY"]), mock.patch("framebridge_expanded.connection_background.run_foreground", return_value=0) as run:
rc = tunnel.connect_chatgpt("/binary", "/state", "http://127.0.0.1:20865/mcp", public, output=io.StringIO(), foreground=True)
self.assertEqual(rc, 0)
self.assertIn("org_test", run.call_args.args[0])
self.assertIn("/v1/tunnel", run.call_args.args[0])
def test_non_tty_fails_before_secret_prompt(self):
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=False), mock.patch.object(tunnel.getpass, "getpass") as prompt:
with self.assertRaises(tunnel.TunnelInputError):
tunnel._hidden("Hidden: ")
prompt.assert_not_called()
class ForegroundTests(unittest.TestCase):
def command(self, code):
return [sys.executable, "-u", "-c", code,
"--control-plane.tunnel-id", ID,
"--mcp.server-url", "url=http://127.0.0.1:20865/mcp"]
def test_fake_key_only_in_child_environment_and_private_log(self):
secret = "FAKE_RUNTIME_ONLY_0123456789"
seen = {}
real_popen = subprocess.Popen
def spawn(args, **kwargs):
seen["args"] = list(args)
seen["env"] = dict(kwargs["env"])
return real_popen(args, **kwargs)
code = "import os,time;print('TECHNICAL_DEBUG '+os.environ['" + tunnel.KEY_ENV + "'],flush=True);time.sleep(.3)"
with tempfile.TemporaryDirectory(prefix="tunnel-output-") as td:
output = io.StringIO()
with mock.patch("framebridge_expanded.connection_monitor.probe", return_value=("ready", "Verified synthetic test state")):
result = tunnel.run_foreground(self.command(code), secret, state=td, output=output, popen=spawn)
data = (Path(td) / "logs/chatgpt/connection.log").read_text()
self.assertEqual(result, 0)
self.assertEqual(seen["env"][tunnel.KEY_ENV], secret)
self.assertNotIn(secret, repr(seen["args"]))
self.assertNotIn(secret, output.getvalue())
self.assertNotIn("TECHNICAL_DEBUG", output.getvalue())
self.assertIn("TECHNICAL_DEBUG [REDACTED]", data)
self.assertNotIn(secret, data)
self.assertIn("[OK]", output.getvalue())
self.assertIn("successful tool call", output.getvalue())
self.assertEqual((Path(td) / "logs/chatgpt/connection.log").stat().st_mode & 0o777, 0o600)
def test_ctrl_c_stops_only_owned_child(self):
real_popen = subprocess.Popen
seen = {}
def spawn(args, **kwargs):
seen["process"] = real_popen(args, **kwargs)
return seen["process"]
with tempfile.TemporaryDirectory(prefix="tunnel-stop-") as td:
with mock.patch("framebridge_expanded.connection_monitor.probe", side_effect=KeyboardInterrupt):
rc = tunnel.run_foreground(self.command("import time;time.sleep(60)"), "FAKE_ONLY", state=td,
output=io.StringIO(), popen=spawn)
self.assertEqual(rc, 130)
self.assertIsNotNone(seen["process"].poll())
self.assertFalse(any((Path(td) / "connection-runtime").iterdir()))
def test_interactive_flow_saves_only_public_values(self):
secret = "FAKE_RUNTIME_ONLY_0123456789"
saved = []
output = io.StringIO()
with mock.patch.object(tunnel, "_hidden", side_effect=[ID, secret]), mock.patch("framebridge_expanded.connection_background.run_foreground", return_value=0) as run:
rc = tunnel.connect_chatgpt("/binary", "/state", "http://127.0.0.1:20865/mcp", {"account_label": "test@example.com"}, saved.append, output=output, foreground=True)
self.assertEqual(rc, 0)
self.assertEqual(saved[0]["tunnel_id"], ID)
self.assertNotIn(secret, repr(saved))
self.assertNotIn(secret, output.getvalue())
self.assertEqual(run.call_args.args[1], secret)
self.assertNotIn(secret, repr(run.call_args.args[0]))
class ManagedWizardTests(unittest.TestCase):
@staticmethod
def ready():
return {"configured": True, "enabled": True, "service_active": True,
"service_state": "active", "runtime": {"running": True, "ready": True},
"connection": {"alive": True, "verified": True, "state": "ready"}}
def invoke(self, configure, *, public=None, output=None, wait_timeout=0):
return tunnel.connect_chatgpt("/binary", "/state", "http://127.0.0.1:20865/mcp", public,
output=output or io.StringIO(), configure_connection=configure,
wait_timeout=wait_timeout)
def test_three_fields_are_ordered_and_only_public_settings_are_committed(self):
events = []
def label(prompt):
events.append(prompt)
return "test@example.com"
answers = iter([ID, "SYNTHETIC-MCP-ACCESS-KEY"])
def hidden(prompt):
events.append(prompt)
return next(answers)
seen = []
def configure(state, public, credential):
events.append("commit")
seen.append((state, public, credential))
return self.ready()
output = io.StringIO()
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", side_effect=label), mock.patch.object(tunnel, "_hidden", side_effect=hidden):
rc = self.invoke(configure, output=output)
self.assertEqual(0, rc)
self.assertTrue(events[0].startswith("ChatGPT account (email label)"))
self.assertTrue(events[1].startswith("Tunnel ID (hidden"))
self.assertEqual("MCP server access key (hidden): ", events[2])
self.assertEqual("commit", events[3])
self.assertEqual("test@example.com", seen[0][1]["account_label"])
self.assertEqual(ID, seen[0][1]["tunnel_id"])
self.assertNotIn("SYNTHETIC-MCP-ACCESS-KEY", repr(seen[0][1]))
self.assertNotIn("SYNTHETIC-MCP-ACCESS-KEY", output.getvalue())
self.assertEqual("Connecting...\n[OK] Connected.\n", output.getvalue())
def test_cancel_at_each_field_never_calls_committer(self):
for field in range(3):
with self.subTest(field=field):
configure = mock.Mock()
label = mock.Mock(return_value="test@example.com")
hidden = mock.Mock(side_effect=[ID, "SYNTHETIC-MCP-ACCESS-KEY"])
if field == 0:
label.side_effect = KeyboardInterrupt
elif field == 1:
hidden.side_effect = KeyboardInterrupt
else:
hidden.side_effect = [ID, KeyboardInterrupt]
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", label), mock.patch.object(tunnel, "_hidden", hidden):
rc = self.invoke(configure)
self.assertEqual(130, rc)
configure.assert_not_called()
def test_full_command_in_normal_hidden_id_field_is_rejected_without_echo(self):
configure = mock.Mock()
pasted = "tunnel-client run --tunnel-id " + ID + " --api-key SYNTHETIC-PASTED-SECRET"
output = io.StringIO()
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", return_value="test@example.com"), mock.patch.object(tunnel, "_hidden", return_value=pasted) as hidden:
with self.assertRaises(tunnel.TunnelInputError) as error:
self.invoke(configure, output=output)
self.assertNotIn("SYNTHETIC-PASTED-SECRET", str(error.exception) + output.getvalue())
self.assertEqual(1, hidden.call_count)
configure.assert_not_called()
def test_invalid_key_does_not_commit_and_is_not_echoed(self):
configure = mock.Mock()
output = io.StringIO()
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", return_value="test@example.com"), mock.patch.object(tunnel, "_hidden", side_effect=[ID, "SYNTHETIC INVALID KEY"]):
with self.assertRaises(tunnel.TunnelInputError) as error:
self.invoke(configure, output=output)
self.assertNotIn("SYNTHETIC INVALID KEY", str(error.exception) + output.getvalue())
configure.assert_not_called()
def test_saved_public_defaults_preserve_tunnel_context_but_key_is_requested(self):
public = {"account_label": "test@example.com", "tunnel_id": ID,
"control_plane_organization_id": "org_test", "control_plane_url_path": "/v1/tunnel"}
configure = mock.Mock(return_value=self.ready())
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", return_value=""), mock.patch.object(tunnel, "_hidden", side_effect=["", "SYNTHETIC-MCP-ACCESS-KEY"]) as hidden:
self.assertEqual(0, self.invoke(configure, public=public))
self.assertEqual(2, hidden.call_count)
self.assertEqual(public, configure.call_args.args[1])
def test_pending_returns_prompt_nonzero_without_false_success_or_reboot(self):
pending = self.ready()
pending["connection"] = {"alive": True, "verified": False, "state": "connecting"}
output = io.StringIO()
self.assertEqual(1, tunnel._wait_managed_connection("/state", pending, output, wait_timeout=0))
self.assertIn("[PENDING]", output.getvalue())
self.assertNotIn("[OK]", output.getvalue())
self.assertNotIn("reboot", output.getvalue().lower())
def test_failed_service_cannot_use_stale_verified_connection_as_success(self):
for failure in ({"service_state": "failed", "service_active": False}, {"service_state": "failed", "service_active": True}, {"worker": {"state": "failed"}}):
with self.subTest(failure=failure):
state = self.ready()
state.update(failure)
output = io.StringIO()
self.assertEqual(1, tunnel._wait_managed_connection("/state", state, output, wait_timeout=0))
self.assertIn("[ERROR]", output.getvalue())
self.assertNotIn("[OK]", output.getvalue())
def test_readiness_is_polled_and_key_is_requested_only_once(self):
from framebridge_expanded import autostart
starting = self.ready()
starting["connection"] = {"alive": False, "verified": False, "state": "connecting"}
configure = mock.Mock(return_value=starting)
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", return_value="test@example.com"), mock.patch.object(tunnel, "_hidden", side_effect=[ID, "SYNTHETIC-MCP-ACCESS-KEY"]) as hidden, mock.patch.object(tunnel.time, "sleep"), mock.patch.object(autostart, "status", return_value=self.ready()) as status:
self.assertEqual(0, self.invoke(configure, wait_timeout=2))
self.assertEqual(2, hidden.call_count)
self.assertEqual("MCP server access key (hidden): ", hidden.call_args.args[0])
status.assert_called_once_with("/state")
def test_interrupt_during_wait_reports_saved_background_connection(self):
from framebridge_expanded import autostart
starting = self.ready()
starting["connection"] = {"alive": False, "verified": False, "state": "connecting"}
output = io.StringIO()
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", return_value="test@example.com"), mock.patch.object(tunnel, "_hidden", side_effect=[ID, "SYNTHETIC-MCP-ACCESS-KEY"]), mock.patch.object(tunnel.time, "sleep"), mock.patch.object(autostart, "status", side_effect=KeyboardInterrupt):
self.assertEqual(130, self.invoke(mock.Mock(return_value=starting), output=output, wait_timeout=2))
self.assertIn("continues in the background", output.getvalue())
self.assertNotIn("Connection cancelled", output.getvalue())
def test_interrupt_after_commit_does_not_report_connection_cancelled(self):
marker = {"configured": True, "enabled": True, "interrupted_after_commit": True}
configure = mock.Mock(return_value=marker)
output = io.StringIO()
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=True), mock.patch("builtins.input", return_value="test@example.com"), mock.patch.object(tunnel, "_hidden", side_effect=[ID, "SYNTHETIC-MCP-ACCESS-KEY"]):
self.assertEqual(130, self.invoke(configure, output=output))
self.assertIn("continues in the background", output.getvalue())
self.assertNotIn("cancelled", output.getvalue().lower())
self.assertNotIn("[OK]", output.getvalue())
def test_resume_status_interrupted_after_commit_is_not_reported_cancelled(self):
marker = {"configured": True, "enabled": True, "interrupted_after_commit": True}
output = io.StringIO()
self.assertEqual(130, tunnel.resume_managed_connection("/state", mock.Mock(return_value=marker), output=output, wait_timeout=0))
self.assertIn("continues in the background", output.getvalue())
self.assertNotIn("cancelled", output.getvalue().lower())
def test_non_tty_fails_before_any_input_or_commit(self):
configure = mock.Mock()
with mock.patch.object(tunnel.sys.stdin, "isatty", return_value=False), mock.patch("builtins.input") as label, mock.patch.object(tunnel, "_hidden") as hidden:
with self.assertRaises(tunnel.TunnelInputError):
self.invoke(configure)
label.assert_not_called()
hidden.assert_not_called()
configure.assert_not_called()
def test_resume_uses_no_setup_prompts(self):
resume = mock.Mock(return_value=self.ready())
with mock.patch("builtins.input") as label, mock.patch.object(tunnel, "_hidden") as hidden:
self.assertEqual(0, tunnel.resume_managed_connection("/state", resume, output=io.StringIO(), wait_timeout=0))
label.assert_not_called()
hidden.assert_not_called()
resume.assert_called_once_with("/state")
if __name__ == "__main__":
unittest.main()
+109
View File
@@ -0,0 +1,109 @@
import hashlib
import os
from pathlib import Path
import pwd
import subprocess
import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
from framebridge_expanded import uninstall
class UninstallTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.home = Path(self.temp.name)
self.directory = self.home / ".config/systemd/user"
self.directory.mkdir(parents=True)
def unit(self, state=None):
state = str(state or self.home / "state")
name = "framebridge-autostart-" + hashlib.sha256(state.encode()).hexdigest()[:16] + ".service"
path = self.directory / name
quoted_state = state.replace("$", "$$").replace("%", "%%").replace("\\", "\\\\").replace('"', '\\"')
path.write_text('# FrameBridge managed autostart v1\n[Service]\nEnvironment="PYTHONPATH=' + uninstall.MODULE_ROOT + '"\nExecStart="/usr/bin/python3" "-B" "-m" "framebridge_expanded.autostart" "_run" "--state" "' + quoted_state + '"\n[Install]\nWantedBy=default.target\n')
return path
def test_owned_service_matches_even_with_escaped_state(self):
path = self.unit(self.home / "a space $ and % value")
self.assertTrue(uninstall.managed_unit(path, os.getuid()))
def test_wrong_owner_is_not_accepted(self):
self.assertFalse(uninstall.managed_unit(self.unit(), os.getuid() + 1))
def test_foreign_command_and_missing_marker_are_not_accepted(self):
p = self.unit()
p.write_text(p.read_text().replace('"framebridge_expanded.autostart"', '"another_application"'))
self.assertFalse(uninstall.managed_unit(p, os.getuid()))
p = self.unit();p.write_text(p.read_text().replace('# FrameBridge managed autostart v1', '# foreign'))
self.assertFalse(uninstall.managed_unit(p, os.getuid()))
def test_symlink_and_wrong_hash_are_not_accepted(self):
p = self.unit();data = p.read_text();p.unlink();target = self.home / "outside";target.write_text(data);p.symlink_to(target)
self.assertFalse(uninstall.managed_unit(p, os.getuid()))
p.unlink();p.write_text(data);other = self.directory / ("framebridge-autostart-" + "f" * 16 + ".service");p.rename(other)
self.assertFalse(uninstall.managed_unit(other, os.getuid()))
def test_live_cleanup_stops_and_disables_only_owned_unit(self):
p = self.unit();foreign = self.directory / "unrelated.service";foreign.write_text("unrelated")
run = mock.Mock(return_value=SimpleNamespace(returncode=0, stdout=b""))
self.assertEqual(uninstall.cleanup_user(self.home, run=run, bus_available=True), 1)
calls = [c.args[0] for c in run.call_args_list]
self.assertEqual(calls[0], ["/usr/bin/systemctl", "--user", "stop", p.name])
self.assertEqual(calls[1], ["/usr/bin/systemctl", "--user", "--no-reload", "disable", p.name])
self.assertEqual(foreign.read_text(), "unrelated")
self.assertTrue(p.is_file())
def test_actual_offline_disable_preserves_profile_and_cipher(self):
p = self.unit();wants = self.directory / "default.target.wants";wants.mkdir();link = wants / p.name;link.symlink_to(Path("..") / p.name)
profile = self.home / "profile.json";cipher = self.home / "credential.cred";profile.write_text('profile fixture');cipher.write_bytes(b'encrypted fixture only')
self.assertEqual(uninstall.cleanup_user(self.home, bus_available=False), 1)
self.assertFalse(link.is_symlink())
self.assertEqual(profile.read_text(), 'profile fixture')
self.assertEqual(cipher.read_bytes(), b'encrypted fixture only')
self.assertTrue(p.is_file())
def test_failed_stop_still_attempts_disable_and_reports_failure(self):
self.unit()
def invoke(args, **kwargs):
if "stop" in args:return SimpleNamespace(returncode=1, stdout=b"")
if "show" in args:return SimpleNamespace(returncode=0, stdout=b"active\n")
return SimpleNamespace(returncode=0, stdout=b"")
run = mock.Mock(side_effect=invoke)
with self.assertRaises(RuntimeError):uninstall.cleanup_user(self.home, run=run, bus_available=True)
self.assertTrue(any("disable" in c.args[0] for c in run.call_args_list))
def test_no_managed_services_makes_no_calls(self):
run = mock.Mock();self.assertEqual(uninstall.cleanup_user(self.home, run=run), 0);run.assert_not_called()
def test_all_users_drops_privileges_and_includes_service_accounts(self):
accounts = [SimpleNamespace(pw_name="testservice", pw_uid=104, pw_dir="/var/lib/testservice"), SimpleNamespace(pw_name="testperson", pw_uid=1000, pw_dir="/home/testperson"), SimpleNamespace(pw_name="nobody", pw_uid=65534, pw_dir="/nonexistent")]
run = mock.Mock(return_value=SimpleNamespace(returncode=0))
with mock.patch.object(uninstall.os, "getuid", return_value=0):uninstall.cleanup_all_users(run=run, accounts=accounts)
self.assertEqual(run.call_count, 2)
for call, name in zip(run.call_args_list, ["testservice", "testperson"]):
args = call.args[0]
self.assertEqual(args[:7], ["/usr/sbin/runuser", "-u", name, "--", "/usr/bin/env", "-i", "HOME=" + ("/var/lib/testservice" if name == "testservice" else "/home/testperson")])
self.assertEqual(args[-4:], ["-I", "-S", str(Path(uninstall.__file__).resolve()), "--user"])
def test_root_dispatch_continues_after_one_user_failure(self):
accounts = [SimpleNamespace(pw_name="first", pw_uid=1000, pw_dir="/home/first"), SimpleNamespace(pw_name="second", pw_uid=1001, pw_dir="/home/second")]
run = mock.Mock(side_effect=[SimpleNamespace(returncode=1), SimpleNamespace(returncode=0)])
with mock.patch.object(uninstall.os, "getuid", return_value=0):
with self.assertRaises(RuntimeError):uninstall.cleanup_all_users(run=run, accounts=accounts)
self.assertEqual(run.call_count, 2)
def test_nonroot_cannot_dispatch_other_users(self):
with mock.patch.object(uninstall.os, "getuid", return_value=1000):
with self.assertRaises(RuntimeError):uninstall.cleanup_all_users(accounts=[])
def test_stop_timeout_still_disables_unit(self):
self.unit()
def invoke(args, **kwargs):
if "stop" in args:raise subprocess.TimeoutExpired(args, 35)
return SimpleNamespace(returncode=0, stdout=b"")
run = mock.Mock(side_effect=invoke)
with self.assertRaises(RuntimeError):uninstall.cleanup_user(self.home, run=run, bus_available=True)
self.assertTrue(any("disable" in c.args[0] for c in run.call_args_list))