FrameBridge 0.3.4 corresponding-source release

Snapshot assembled only from the signed corresponding-source archive.
Source archive SHA-256: db91a270cdefc4be79056ed38a0b4a16747e1ab130592645eac40fa3809809df
This commit is contained in:
FrameBridge release automation committed 2026-10-29 11:53:40 -04:00
1 parent b8c9b713d8
commit cdd980e1eb
993 files changed
+255481 -1720

No files matched your search

+21 -1
View File
@@ -31,7 +31,8 @@ class CandidateArchiveTests(unittest.TestCase):
mock.patch.object(archive_sources, "validate_vendor", return_value=evidence), \
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()) as stderr:
archive_sources.main(args)
return self.base / "release/candidate-source" / archive_sources.ARCHIVE_NAME, stderr.getvalue()
directory = "public-source" if "--public-release" in args else "candidate-source"
return self.base / "release" / directory / archive_sources.ARCHIVE_NAME, stderr.getvalue()
def test_default_archive_declares_missing_alternate_abis(self):
output, warning = self.run_archive([])
@@ -64,6 +65,25 @@ class CandidateArchiveTests(unittest.TestCase):
self.assertEqual(set(data["vendors"]), {"cp312", "cp313", "cp314"})
self.assertEqual(data["omitted_vendor_abis"], [])
def test_public_archive_requires_matching_cp312_and_excludes_experimental_input(self):
for args in (["--public-release"], ["--public-release", "--vendor-cp312", str(self.root), "--vendor-cp314", str(self.root)]):
with self.assertRaises(SystemExit):
self.run_archive(args)
def test_public_archive_describes_three_profiles_without_claiming_publication(self):
vendor = self.root / "cp312"; vendor.mkdir()
(vendor / "module.py").write_text("# reviewed cp312 input\n")
output, warning = self.run_archive(["--public-release", "--vendor-cp312", str(vendor)])
self.assertFalse(warning)
manifest = json.loads((output.parent / "SOURCE-ARCHIVE-MANIFEST.json").read_text())
self.assertEqual(manifest["status"], "RELEASE_SOURCE_ASSEMBLED")
self.assertEqual(set(manifest["vendor_inputs"]["vendors"]), {"cp312", "cp313"})
with tarfile.open(output) as archive:
readme = archive.extractfile(archive_sources.ARCHIVE_ROOT + "/README-SOURCE-ARCHIVE.md").read().decode()
self.assertIn("--public-release --source-manifest", readme)
self.assertNotIn("compatibility candidate source", readme)
self.assertIn("Signatures, publication", readme)
def test_alternate_vendor_symlink_is_rejected(self):
vendor = self.root / "cp312"; vendor.mkdir()
(vendor / "unsafe.py").symlink_to(self.input)
+239 -1
View File
@@ -9,7 +9,7 @@ import subprocess
import tempfile
import unittest
from unittest import mock
from framebridge_expanded import autostart, config, runtime, connection_background
from framebridge_expanded import autostart, config, runtime, connection_background, credential_helper
ID = "tunnel_0123456789abcdef0123456789abcdef"
MARKER = "FRAMEBRIDGE-SYNTHETIC-NOT-A-REAL-KEY-AUTOSTART"
@@ -435,6 +435,161 @@ class AutostartTests(unittest.TestCase):
self.assertNotIn("LoadCredentialEncrypted", unit.read_text())
self.assertFalse((directory / "credential.cred").exists())
def test_fresh_node_with_existing_linger_authenticates_before_existing_grant_cleanup(self):
for start_now in (False, True):
with self.subTest(start_now=start_now):
state = self.home / ("node-now" if start_now else "node-boot")
config.save_config(state, config.default_config(role="node"))
before = (state / "config.json").read_bytes()
self.calls.clear()
self.system_calls.clear()
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(autostart, "_node_helper_cleanup_required", return_value=True), mock.patch.object(autostart, "_credential_backend", side_effect=AssertionError("A node has no connection credential")):
autostart._enable(state, start_now=start_now, confirm=False)
_, directory, unit = autostart._paths(state)
expected_prune = ["sudo", "-n", "--", str(autostart.HELPER), "prune", autostart._profile_id(state), "-"]
# Exercise the real prune implementation through the command
# boundary: no authentication or global boot change is hidden.
self.assertEqual([["sudo", "-v"], expected_prune], [args for args, _ in self.calls])
self.assertEqual(before, (state / "config.json").read_bytes())
self.assertTrue(autostart._manifest(directory)["linger_before"])
self.assertTrue(autostart._manifest(directory)["node"])
self.assertFalse((directory / "credential.cred").exists())
self.assertIn(("enable", "--now", unit.name) if start_now else ("enable", unit.name), self.system_calls)
self.assertEqual(0o600, stat.S_IMODE(unit.stat().st_mode))
self.inputs.assert_not_called()
self.key_prompt.assert_not_called()
def test_fresh_node_with_existing_linger_fails_closed_when_prune_is_not_authorized(self):
config.save_config(self.state, config.default_config(role="node"))
before = (self.state / "config.json").read_bytes()
def denied_prune(args, **kwargs):
if len(args) > 4 and args[:4] == ["sudo", "-n", "--", str(autostart.HELPER)] and args[4] == "prune":
self.calls.append((args, kwargs))
raise RuntimeError("Synthetic cleanup grant denied")
return self.fake_command(args, **kwargs)
self.command.side_effect = denied_prune
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(autostart, "_node_helper_cleanup_required", return_value=True):
with self.assertRaisesRegex(RuntimeError, "cleanup grant denied"):
autostart._enable(self.state, confirm=False)
_, directory, unit = autostart._paths(self.state)
self.assertEqual(before, (self.state / "config.json").read_bytes())
self.assertFalse(unit.exists())
self.assertFalse((directory / "installation.json").exists())
self.assertEqual([], self.system_calls)
self.assertEqual(["sudo", "-v"], self.calls[0][0])
self.assertFalse(any("loginctl" in args for args, _ in self.calls))
def test_fresh_node_with_existing_linger_and_no_managed_grants_needs_no_privilege(self):
policies = self.root / "policies"
policies.mkdir()
profile = autostart._profile_id(self.state)
# Other profiles and owners must not cause an unnecessary request.
(policies / ("framebridge-credential-" + str(os.getuid()) + "-" + "a" * 64 + "-" + "b" * 64)).touch()
(policies / ("framebridge-credential-987654321-" + profile + "-" + "b" * 64)).touch()
config.save_config(self.state, config.default_config(role="node"))
before = (self.state / "config.json").read_bytes()
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(credential_helper, "SUDOERS", policies), mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path):
autostart._enable(self.state, confirm=False, start_now=False)
_, directory, unit = autostart._paths(self.state)
self.assertEqual([], self.calls)
self.assertEqual(before, (self.state / "config.json").read_bytes())
self.assertIn(("enable", unit.name), self.system_calls)
self.assertTrue(autostart._manifest(directory)["node"])
self.assertFalse((directory / "credential.cred").exists())
self.key_prompt.assert_not_called()
def test_ambiguous_node_policy_fails_before_authentication_or_managed_writes(self):
policies = self.root / "policies"
policies.mkdir()
name = "framebridge-credential-authorize-" + str(os.getuid()) + "-" + autostart._profile_id(self.state) + "-unknown"
(policies / name).touch()
config.save_config(self.state, config.default_config(role="node"))
before = (self.state / "config.json").read_bytes()
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(credential_helper, "SUDOERS", policies), mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path):
with self.assertRaisesRegex(RuntimeError, "ambiguous.*administrator review"):
autostart._enable(self.state, confirm=False)
_, directory, unit = autostart._paths(self.state)
self.assertEqual([], self.calls)
self.assertEqual([], self.system_calls)
self.assertEqual(before, (self.state / "config.json").read_bytes())
self.assertFalse(unit.exists())
self.assertFalse((directory / "installation.json").exists())
self.assertFalse((directory / "credential.cred").exists())
self.assertTrue((policies / name).exists())
def test_fresh_node_without_linger_requires_authentication_before_managed_writes(self):
config.save_config(self.state, config.default_config(role="node"))
before = (self.state / "config.json").read_bytes()
def denied_authentication(args, **kwargs):
if args == ["sudo", "-v"]:
self.calls.append((args, kwargs))
raise RuntimeError("Synthetic owner authentication required")
return self.fake_command(args, **kwargs)
self.command.side_effect = denied_authentication
with mock.patch.object(autostart, "_helper_present", return_value=True):
with self.assertRaisesRegex(RuntimeError, "owner authentication required"):
autostart._enable(self.state, confirm=False)
_, directory, unit = autostart._paths(self.state)
self.assertEqual([["sudo", "-v"]], [args for args, _ in self.calls])
self.assertEqual(before, (self.state / "config.json").read_bytes())
self.assertFalse(unit.exists())
self.assertFalse((directory / "installation.json").exists())
self.assertFalse((directory / "credential.cred").exists())
self.assertEqual([], self.system_calls)
self.key_prompt.assert_not_called()
def test_fresh_node_without_linger_keeps_administrative_boot_setup(self):
config.save_config(self.state, config.default_config(role="node"))
with mock.patch.object(autostart, "_helper_present", return_value=True):
autostart._enable(self.state, confirm=False)
user = autostart.pwd.getpwuid(os.getuid()).pw_name
expected_prune = ["sudo", "-n", "--", str(autostart.HELPER), "prune", autostart._profile_id(self.state), "-"]
self.assertEqual([["sudo", "-v"], expected_prune,
["sudo", "-n", "--", "loginctl", "enable-linger", user]],
[args for args, _ in self.calls])
self.key_prompt.assert_not_called()
def test_central_with_existing_linger_still_authenticates_and_protects_its_key(self):
settings = config.default_config(role="central")
settings.update(tunnel_id=ID, headless="auto")
config.save_config(self.state, settings)
with mock.patch.object(autostart, "_linger", return_value=True):
autostart.enable(self.state)
self.assertIn(["sudo", "-v"], [args for args, _ in self.calls])
encryption = [(args, options) for args, options in self.calls if "encrypt" in args and "-ll" not in args]
self.assertEqual(1, len(encryption))
self.assertEqual(MARKER.encode(), encryption[0][1]["data"])
self.assertNotIn(MARKER, " ".join(encryption[0][0]))
self.key_prompt.assert_called_once_with("Connection key (hidden): ")
_, directory, _ = autostart._paths(self.state)
self.assertTrue((directory / "credential.cred").exists())
def test_central_existing_linger_does_not_bypass_failed_owner_authentication(self):
settings = config.default_config(role="central")
settings.update(tunnel_id=ID, headless="auto")
config.save_config(self.state, settings)
before = (self.state / "config.json").read_bytes()
def denied_authentication(args, **kwargs):
if args == ["sudo", "-v"]:
raise RuntimeError("Synthetic owner authentication required")
return self.fake_command(args, **kwargs)
self.command.side_effect = denied_authentication
with mock.patch.object(autostart, "_linger", return_value=True):
with self.assertRaisesRegex(RuntimeError, "owner authentication required"):
autostart.enable(self.state)
_, directory, unit = autostart._paths(self.state)
self.assertEqual(before, (self.state / "config.json").read_bytes())
self.assertFalse(unit.exists())
self.assertFalse((directory / "installation.json").exists())
self.assertFalse((directory / "credential.cred").exists())
self.key_prompt.assert_not_called()
def test_private_file_reader_rejects_symlink_hardlink_fifo_and_public_file(self):
original = self.root / "original"
original.write_bytes(b"safe")
@@ -466,6 +621,89 @@ class AutostartTests(unittest.TestCase):
finally:
os.close(fd)
def _service_directory_owners(self, owners):
actual_lstat = Path.lstat
def owner_lstat(path, *args, **kwargs):
values = list(actual_lstat(path, *args, **kwargs))
values[4] = owners.get(path, values[4])
return os.stat_result(values)
return mock.patch.object(Path, "lstat", owner_lstat)
def test_root_owned_service_ancestors_allow_owned_final_directory_without_changes(self):
final = self.home / ".config/systemd/user"
final.mkdir(parents=True, mode=0o700)
directories = [self.home / ".config", final.parent, final]
before = {p: (p.stat().st_uid, p.stat().st_mode) for p in directories}
owners = {directories[0]: 0, directories[1]: 0, final: 1000}
with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners):
autostart._check_unit_directory(final / "test.service")
self.assertEqual(before, {p: (p.stat().st_uid, p.stat().st_mode) for p in directories})
self.assertEqual([], list(final.iterdir()))
def test_root_or_other_account_cannot_own_final_service_directory(self):
final = self.home / ".config/systemd/user"
final.mkdir(parents=True, mode=0o700)
for owner in (0, 2000):
with self.subTest(owner=owner), mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners({self.home / ".config": 0, final.parent: 0, final: owner}):
with self.assertRaisesRegex(RuntimeError, "final directory must belong"):
autostart._check_unit_directory(final / "test.service")
self.assertEqual([], list(final.iterdir()))
def test_other_account_cannot_own_service_ancestors(self):
final = self.home / ".config/systemd/user"
final.mkdir(parents=True, mode=0o700)
for ancestor in (self.home / ".config", final.parent):
owners = {self.home / ".config": 0, final.parent: 0, final: 1000, ancestor: 2000}
with self.subTest(ancestor=ancestor.name), mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners):
with self.assertRaisesRegex(RuntimeError, "safe owner"):
autostart._check_unit_directory(final / "test.service")
def test_root_owned_service_ancestors_still_reject_writable_or_symbolic_directories(self):
final = self.home / ".config/systemd/user"
final.mkdir(parents=True, mode=0o700)
directories = [self.home / ".config", final.parent, final]
owners = {directories[0]: 0, directories[1]: 0, final: 1000}
for directory in directories:
original = directory.stat().st_mode
for mode in (0o775, 0o777):
with self.subTest(directory=directory.name, mode=mode):
directory.chmod(mode)
with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners):
with self.assertRaisesRegex(RuntimeError, "not be writable"):
autostart._check_unit_directory(final / "test.service")
self.assertEqual(mode, stat.S_IMODE(directory.stat().st_mode))
directory.chmod(stat.S_IMODE(original))
moved = directory.with_name(directory.name + "-target")
directory.rename(moved)
directory.symlink_to(moved, target_is_directory=True)
try:
with self.subTest(directory=directory.name, symbolic=True):
with self.assertRaisesRegex(RuntimeError, "symbolic links"):
autostart._check_unit_directory(final / "test.service")
finally:
directory.unlink()
moved.rename(directory)
def test_root_owned_parent_creation_failure_preserves_permissions_and_explains_failure(self):
parent = self.home / ".config/systemd"
parent.mkdir(parents=True, mode=0o755)
final = parent / "user"
before = {p: (p.stat().st_uid, p.stat().st_mode) for p in (parent.parent, parent)}
actual_mkdir = Path.mkdir
def denied_final(path, *args, **kwargs):
if path == final:
raise PermissionError("Synthetic administrator-owned parent")
return actual_mkdir(path, *args, **kwargs)
with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners({parent.parent: 0, parent: 0}), mock.patch.object(Path, "mkdir", denied_final):
with self.assertRaisesRegex(RuntimeError, "cannot create.*left unchanged"):
autostart._check_unit_directory(final / "test.service")
self.assertFalse(final.exists())
self.assertEqual(before, {p: (p.stat().st_uid, p.stat().st_mode) for p in before})
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)
+33
View File
@@ -0,0 +1,33 @@
"""The public vendor name root_model.py is not the build user's home directory."""
import importlib.util
from pathlib import Path
import sys
import unittest
PACKAGING = Path(__file__).resolve().parents[1] / "packaging"
sys.path.insert(0, str(PACKAGING))
spec = importlib.util.spec_from_file_location("privacy_test_builder", PACKAGING / "build_expanded.py")
builder = importlib.util.module_from_spec(spec)
spec.loader.exec_module(builder)
class BuildPrivacyPathTests(unittest.TestCase):
def test_public_module_name_does_not_block_a_root_user_build(self):
data = b'{"path":"source/vendor/pydantic/root_model.py"}'
self.assertFalse(builder.contains_private_path(data, "/root"))
def test_actual_home_paths_are_still_rejected(self):
for data in (b'"/root/private-file"', b'"/root"', b'input: /root\n',
b'file:///root/private-file', b'"/root/"'):
with self.subTest(data=data):
self.assertTrue(builder.contains_private_path(data, "/root"))
def test_neighboring_names_are_not_this_home_directory(self):
for data in (b'"/home/tester-other/file"', b'"/home/tester_backup/file"'):
self.assertFalse(builder.contains_private_path(data, "/home/tester"))
self.assertTrue(builder.contains_private_path(b'"/home/tester/file"', "/home/tester"))
if __name__ == "__main__":
unittest.main()
+70
View File
@@ -0,0 +1,70 @@
"""Exercise the public health command without starting a profile or transport."""
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, health, __version__
class HealthCliTests(unittest.TestCase):
def invoke(self, arguments):
output, errors = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
code = cli.main(arguments)
return code, output.getvalue(), errors.getvalue()
def test_absent_profile_is_reported_without_creating_it(self):
with tempfile.TemporaryDirectory() as temporary:
profile = Path(temporary) / "absent-profile"
code, text, _ = self.invoke(["--state-dir", str(profile), "--json", "health"])
self.assertFalse(profile.exists())
value = json.loads(text)
self.assertEqual(code, 1)
self.assertEqual(value["version"], __version__)
self.assertEqual(value["command"], "health")
self.assertTrue(value["ok"])
self.assertEqual(value["result"]["health"]["state"], "unconfigured")
self.assertFalse(value["result"]["health"]["healthy"])
self.assertTrue(value["result"]["read_only"])
def test_query_success_does_not_make_exit_status_healthy(self):
for state, healthy, expected in (("healthy", True, 0), ("paused", True, 0),
("degraded", False, 1), ("unavailable", None, 1)):
with self.subTest(state=state):
report = {"ok": True, "health": {"state": state, "healthy": healthy}}
module = SimpleNamespace(collect=mock.Mock(return_value=report))
with mock.patch.object(cli, "_module", return_value=module):
code, text, _ = self.invoke(["health", "--json"])
self.assertEqual(code, expected)
self.assertEqual(json.loads(text)["result"], report)
def test_failed_observation_cannot_pass_even_with_a_healthy_flag(self):
module = SimpleNamespace(collect=mock.Mock(return_value={"ok": False, "health": {"healthy": True}}))
with mock.patch.object(cli, "_module", return_value=module):
code, text, _ = self.invoke(["--json", "health"])
self.assertEqual(code, 1)
self.assertFalse(json.loads(text)["ok"])
def test_human_summary_names_the_local_machine_and_its_limits(self):
with tempfile.TemporaryDirectory() as temporary:
code, text, _ = self.invoke(["health", "--state-dir", str(Path(temporary) / "absent")])
self.assertEqual(code, 1)
self.assertIn("Target: LOCAL = this computer", text)
self.assertIn("unconfigured", text)
self.assertIn("not a fleet scan", text)
def test_json_output_never_prompts_for_authentication(self):
with tempfile.TemporaryDirectory() as temporary, mock.patch("builtins.input") as ask:
code, text, _ = self.invoke(["health", "--json", "--state-dir", str(Path(temporary) / "absent")])
ask.assert_not_called()
self.assertEqual(code, 1)
self.assertIsNone(json.loads(text)["result"]["software"]["runtime_version"])
if __name__ == "__main__":
unittest.main()
@@ -141,6 +141,11 @@ class BackgroundTests(unittest.TestCase):
record=json.loads(path.read_text());record['verified_at']=time.time()-30
record['message']='\x1b[31m'+FAKE
background._write(path,record)
if record.get('heartbeat_backend') == background._HEARTBEAT_BACKEND:
heartbeat_path=background._heartbeat_directory(self.state)/'status.json'
heartbeat=background._read(heartbeat_path)
heartbeat['verified_at']=time.time()-30
background._write(heartbeat_path,heartbeat)
current=background.status(self.state)
self.assertFalse(current['verified'])
self.assertEqual(current['state'],'retrying')
+96
View File
@@ -1,4 +1,5 @@
"""Routing contract tests; original read provider is exercised locally."""
import base64
import hashlib
from pathlib import Path
import socket
@@ -10,6 +11,8 @@ from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "expanded"))
from framebridge_expanded import fleet
from framebridge_expanded.real_screen.screen_frontdoor import RPC_COMMAND
from framebridge_expanded.real_screen.screen_rpc import encode
LOCAL_ID = "c6d827a4-219f-48fc-82d8-f410176313ca"
REMOTE_ID = "6d88c5ae-343e-4413-a202-ccae2ae7694a"
@@ -134,6 +137,99 @@ class FleetRoutingTests(unittest.TestCase):
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")
@staticmethod
def screen_script():
return RPC_COMMAND + base64.b64encode(encode({"op": "observe"})).decode("ascii") + "\n"
def assert_identity(self, value, requested, local=False):
self.assertEqual(value["requested_target"], requested)
self.assertEqual(value["resolved_target"], "test-central" if local else "test-node")
self.assertEqual(value["node"], value["resolved_target"])
self.assertEqual(value["target"], "LOCAL" if local else "test-node")
self.assertEqual(value["node_id"], LOCAL_ID if local else REMOTE_ID)
self.assertEqual(value["hostname"], socket.gethostname() if local else "test-node")
self.assertEqual(value["transport"], "local" if local else "broker_outbound")
@staticmethod
def misleading_response(payload=None):
value = {"ok": True, "target": "wrong-machine", "node": "wrong-machine",
"requested_target": "wrong-machine", "resolved_target": "wrong-machine",
"node_id": "wrong-id", "hostname": "wrong-host", "transport": "wrong-route"}
if payload is not None:
value.update(job_id=payload["job_id"], command_sha256=payload.get("sha256"))
return value
def test_read_identity_comes_from_confirmed_route(self):
for requested, local in [("LOCAL", True), ("test-central", True), (LOCAL_ID, True),
("test-node", False), (REMOTE_ID, False)]:
with self.subTest(requested=requested), \
mock.patch.object(fleet, "read_local", return_value=self.misleading_response()), \
mock.patch("framebridge_expanded.broker.request", return_value=self.misleading_response()):
value = fleet.node_read(requested, "system.status", {})
self.assertTrue(value["ok"])
self.assert_identity(value, requested, local)
self.assertEqual(value["node"], value["resolved_target"])
def test_execution_routes_overwrite_upstream_identity(self):
def remote_response(node_id, capability, payload, timeout_s):
return self.misleading_response(payload)
calls = [
lambda alias: fleet.execute_node(alias, "printf ok", True, job_id="identity-exec"),
lambda alias: fleet.execute_screen_node(alias, self.screen_script(), True,
job_id="identity-screen", include_stdout=True),
lambda alias: fleet.cua_node(alias, {"action": "OBSERVE"}, True, job_id="identity-cua"),
]
for route in calls:
for requested, local in [("LOCAL", True), ("test-central", True), (LOCAL_ID, True),
("test-node", False), (REMOTE_ID, False)]:
with self.subTest(route=route, requested=requested), \
mock.patch("framebridge_expanded.runtime.adapter_request",
side_effect=self.misleading_response), \
mock.patch("framebridge_expanded.broker.request", side_effect=remote_response):
value = route(requested)
self.assertTrue(value["ok"], value)
self.assert_identity(value, requested, local)
def test_unknown_target_never_falls_back_to_local(self):
with mock.patch("framebridge_expanded.runtime.adapter_request") as local, \
mock.patch("framebridge_expanded.broker.request") as remote:
values = [
fleet.node_read("unknown-node", "system.status", {}),
fleet.execute_node("unknown-node", "printf ok", True),
fleet.execute_screen_node("unknown-node", self.screen_script(), True, include_stdout=True),
fleet.cua_node("unknown-node", {"action": "OBSERVE"}, True),
]
local.assert_not_called()
remote.assert_not_called()
for value in values:
self.assertFalse(value["ok"])
self.assertEqual(value["error"], "NODE_NOT_FOUND")
self.assertNotIn("resolved_target", value)
self.assertNotIn("node_id", value)
def test_offline_target_errors_retain_confirmed_identity(self):
self.remote["operational_transport_ready"] = False
with mock.patch("framebridge_expanded.broker.request") as remote:
values = [
fleet.node_read(REMOTE_ID, "system.status", {}),
fleet.execute_node(REMOTE_ID, "printf ok", True),
fleet.execute_screen_node(REMOTE_ID, self.screen_script(), True, include_stdout=True),
fleet.cua_node(REMOTE_ID, {"action": "OBSERVE"}, True),
]
remote.assert_not_called()
for value in values:
self.assertFalse(value["ok"])
self.assertEqual(value["error"], "NODE_OFFLINE")
self.assert_identity(value, REMOTE_ID)
def test_pre_resolution_failure_does_not_claim_identity(self):
with mock.patch.object(fleet, "resolve_node") as lookup:
value = fleet.execute_node("test-node", "printf ok", False)
lookup.assert_not_called()
self.assertEqual(value["error"], "AUTHORIZED_TRUE_REQUIRED")
for field in ("requested_target", "resolved_target", "node_id", "hostname", "transport"):
self.assertNotIn(field, value)
if __name__ == "__main__":
unittest.main()
+196
View File
@@ -0,0 +1,196 @@
import copy
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "expanded"))
from framebridge_expanded import health
NODE_ID = "51f0acf5-3633-41f6-aea7-4fed5cffcb01"
def snapshots():
return ({"ok": True, "configured": True, "role": "central", "name": "gateway",
"runtime": {"running": True, "ready": True, "node_id": NODE_ID,
"display_context": {"kind": "text"},
"capabilities": {"fbexec": True, "fbterm": True, "cua_tty_module": True, "graphical_cua": False}},
"connection": {"verified": True, "state": "ready"},
"boot": {"configured": True, "enabled": False, "service_state": "active"},
"desired_connection": "connected"},
{"ok": True, "platform_compatibility": {"ok": True, "profile_id": "debian13-amd64-py313", "validation_status": "validated", "issues": []},
"issues": [], "missing_commands": [], "capabilities": {"fbexec": True, "fbterm": True, "cua_tty_module": True, "graphical_cua": False}})
class HealthTests(unittest.TestCase):
def test_release_build_identity_is_retained_without_relabelling_it_validated(self):
status, doctor = snapshots()
doctor["platform_compatibility"]["validation_status"] = "release_build"
value = health.build_report(status, doctor)
self.assertTrue(value["ok"])
self.assertEqual(value["prerequisites"]["validation_status"], "release_build")
self.assertNotEqual(value["prerequisites"]["validation_status"], "validated")
def test_healthy_text_mode_does_not_require_graphical_tools_or_boot_enabled(self):
status, doctor = snapshots()
value = health.build_report(status, doctor, observed_at="2026-10-28T12:00:00+00:00", host="gateway")
self.assertTrue(value["ok"])
self.assertEqual(value["health"], {"state": "healthy", "healthy": True, "issues": []})
self.assertEqual(value["boot"]["policy"], "disabled")
self.assertEqual(value["effective_target"]["node_id"], NODE_ID)
self.assertFalse(value["desktop"]["capture_verified_by_this_report"])
self.assertIsNone(value["software"]["runtime_version"])
def test_query_success_is_not_readiness(self):
status, doctor = snapshots()
status["runtime"].update(running=False, ready=False)
status["connection"].update(verified=False, state="stopped")
value = health.build_report(status, doctor)
self.assertTrue(value["ok"])
self.assertEqual(value["health"]["state"], "degraded")
self.assertFalse(value["health"]["healthy"])
self.assertIn("LOCAL_TOOLS_NOT_READY", value["health"]["issues"])
def test_paused_connection_is_distinct_from_failure(self):
status, doctor = snapshots()
status["desired_connection"] = "disconnected"
status["connection"].update(verified=False, state="stopped")
value = health.build_report(status, doctor)
self.assertEqual(value["health"]["state"], "paused")
self.assertTrue(value["health"]["healthy"])
self.assertEqual(value["health"]["issues"], [])
def test_node_does_not_claim_central_connection_verified(self):
status, doctor = snapshots()
status.update(role="node", desired_connection="not_applicable")
value = health.build_report(status, doctor)
self.assertEqual(value["connection"]["state"], "managed_by_central")
self.assertIsNone(value["connection"]["verified"])
self.assertEqual(value["health"]["state"], "healthy")
def test_requested_pause_does_not_hide_a_still_running_tunnel(self):
status, doctor = snapshots()
status["desired_connection"] = "disconnected"
value = health.build_report(status, doctor)
self.assertEqual(value["health"]["state"], "degraded")
self.assertIn("CONNECTION_INTENT_MISMATCH", value["health"]["issues"])
status["connection"].update(verified=False, state="stopped", alive=True)
value = health.build_report(status, doctor)
self.assertFalse(value["health"]["healthy"])
def test_failed_connection_query_cannot_certify_a_healthy_profile(self):
status, doctor = snapshots()
status["connection"]["ok"] = False
value = health.build_report(status, doctor)
self.assertFalse(value["ok"])
self.assertIsNone(value["health"]["healthy"])
self.assertIn("CONNECTION_STATUS_UNAVAILABLE", value["health"]["issues"])
def test_missing_boot_policy_is_unknown_rather_than_healthy(self):
status, doctor = snapshots()
status["boot"] = {}
value = health.build_report(status, doctor)
self.assertFalse(value["ok"])
self.assertEqual(value["health"]["state"], "unavailable")
def test_missing_runtime_status_is_not_a_stopped_runtime(self):
status, doctor = snapshots()
status["runtime"] = {"status_unavailable": True, "running": False, "ready": False}
value = health.build_report(status, doctor)
self.assertFalse(value["ok"])
self.assertEqual(value["runtime"]["state"], "unavailable")
self.assertEqual(value["health"]["state"], "unavailable")
self.assertIsNone(value["health"]["healthy"])
def test_failed_runtime_query_cannot_certify_ready(self):
status, doctor = snapshots()
status["runtime"]["ok"] = False
value = health.build_report(status, doctor)
self.assertFalse(value["ok"])
self.assertEqual(value["health"]["state"], "unavailable")
def test_failed_profile_query_cannot_claim_unconfigured(self):
value = health.build_report({"ok": False, "configured": False}, None)
self.assertFalse(value["ok"])
self.assertEqual(value["health"]["state"], "unavailable")
def test_missing_diagnostic_result_is_unavailable_not_passed(self):
status, _ = snapshots()
value = health.build_report(status, {})
self.assertFalse(value["ok"])
self.assertEqual(value["health"]["state"], "unavailable")
def test_unconfigured_profile_is_observed_without_running_doctor_or_creating_files(self):
with tempfile.TemporaryDirectory() as directory:
state = Path(directory) / "not-created"
with mock.patch.object(health.runtime, "doctor", side_effect=AssertionError("doctor must not run")):
value = health.collect(state)
self.assertFalse(state.exists())
self.assertTrue(value["ok"])
self.assertEqual(value["health"]["state"], "unconfigured")
self.assertEqual(value["health"]["issues"], [])
self.assertEqual(value["prerequisites"]["state"], "not_run")
def test_diagnostics_failure_retains_status_without_leaking_exception(self):
status, _ = snapshots()
with mock.patch.object(health.lifecycle, "status", return_value=status), \
mock.patch.object(health.runtime, "doctor", side_effect=RuntimeError("secret-access-key")):
value = health.collect(Path("/unused"))
self.assertFalse(value["ok"])
self.assertEqual(value["runtime"]["state"], "ready")
self.assertEqual(value["health"]["state"], "unavailable")
self.assertNotIn("secret-access-key", json.dumps(value))
def test_partial_invalid_profile_does_not_guess_it_is_unconfigured(self):
with mock.patch.object(health.lifecycle, "status", side_effect=ValueError("private-input")), \
mock.patch.object(health.runtime, "doctor") as doctor:
value = health.collect(Path("/unused"))
doctor.assert_not_called()
self.assertFalse(value["ok"])
self.assertEqual(value["health"]["state"], "unavailable")
self.assertNotIn("private-input", json.dumps(value))
def test_allowlist_excludes_connection_material_paths_and_untrusted_free_text(self):
status, doctor = snapshots()
status["tunnel_id"] = "secret-tunnel"
status["runtime"].update(state_dir="/private/secret-path", error="secret-error", access_key="secret-key")
status["runtime"]["display_context"]["authority"] = "/private/secret-authority"
status["connection"].update(message="secret-message", log="/private/secret-log", token="secret-token")
doctor.update(network={"token": "secret-network"}, issues=["secret-issue"])
doctor["platform_compatibility"]["issues"] = ["secret-compatibility"]
result = health.build_report(status, doctor, host="host")
self.assertNotIn("secret-", json.dumps(result))
self.assertEqual(result["prerequisites"]["issue_count"], 1)
self.assertEqual(result["prerequisites"]["compatibility_issue_count"], 1)
def test_prerequisite_failure_is_degraded_without_claiming_no_runtime(self):
status, doctor = snapshots()
doctor.update(ok=False, missing_commands=["tmux"])
value = health.build_report(status, doctor)
self.assertTrue(value["ok"])
self.assertEqual(value["health"]["state"], "degraded")
self.assertEqual(value["runtime"]["state"], "ready")
self.assertEqual(value["prerequisites"]["missing_commands"], ["tmux"])
def test_reports_do_not_modify_snapshots(self):
status, doctor = snapshots()
before = copy.deepcopy((status, doctor))
health.build_report(status, doctor)
self.assertEqual((status, doctor), before)
def test_terminal_output_is_explicit_about_target_and_observation_limit(self):
status, doctor = snapshots()
value = health.build_report(status, doctor, host="gateway")
text = health.render(value)
self.assertIn("Target: LOCAL = this computer (gateway)", text)
self.assertIn(NODE_ID, text)
self.assertIn("capture/input not tested", text)
self.assertIn("not proof of the running version", text)
self.assertIn("not a fleet scan", text)
if __name__ == "__main__":
unittest.main()
+152
View File
@@ -0,0 +1,152 @@
"""Liveness stays fresh without periodically syncing the durable profile."""
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest import mock
from framebridge_expanded import autostart, connection_background as background
class IdleStateIOTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.state = self.root / "profile"
self.directory = background._directory(self.state)
self.runtime_parent = self.root / "runtime"
self.runtime_parent.mkdir(mode=0o700)
(self.runtime_parent / str(os.getuid())).mkdir(mode=0o700)
self.addCleanup(mock.patch.stopall)
mock.patch.object(background, "_RUNTIME_PARENT", self.runtime_parent).start()
self.identity = dict(pid=12345, uid=os.getuid(), start_ticks=99,
boot_id="boot-one", session_id=12345,
process_group=12345, state_dir=str(self.state), lock_fd=8)
def sink(self):
runtime = background._heartbeat_directory(self.state, create=True)
return background._StatusOutput(self.directory / "status.json", self.identity,
runtime / "status.json")
def status(self):
with mock.patch.object(background, "_valid_identity", return_value=True):
return background.status(self.state)
def test_ready_heartbeat_fresh_beyond_timeout_without_durable_writes(self):
clock = [100.0]
with mock.patch.object(background.time, "time", side_effect=lambda: clock[0]), mock.patch.object(background.time, "monotonic", side_effect=lambda: clock[0]):
sink = self.sink()
sink.observe("ready")
sink.change("ready", "[OK] Connected.")
durable = sink.path.stat()
original = sink.path.read_bytes()
for instant in range(101, 141):
clock[0] = float(instant)
sink.observe("ready")
self.assertTrue(self.status()["verified"])
self.assertEqual(sink.path.read_bytes(), original)
self.assertEqual(sink.path.stat().st_ino, durable.st_ino)
self.assertEqual(sink.path.stat().st_mtime_ns, durable.st_mtime_ns)
clock[0] = 156.0
self.assertFalse(self.status()["verified"])
self.assertEqual(self.status()["state"], "retrying")
sink.observe("ready")
self.assertTrue(self.status()["verified"])
def test_failure_and_recovery_publish_transitions_immediately(self):
sink = self.sink()
sink.observe("ready")
sink.change("ready", "[OK] Connected.")
self.assertTrue(self.status()["verified"])
sink.observe("access")
self.assertFalse(self.status()["verified"])
sink.change("retrying", "[RETRYING] Lost readiness.")
self.assertEqual(json.loads(sink.path.read_text())["state"], "retrying")
sink.observe("ready")
sink.change("ready", "[OK] Connected.")
self.assertTrue(self.status()["verified"])
def test_missing_or_old_worker_heartbeat_never_uses_durable_timestamp(self):
sink = self.sink()
sink.observe("ready")
sink.change("ready", "[OK] Connected.")
heartbeat = background._read(sink.heartbeat_path)
for field, value in (("pid", 99999), ("boot_id", "older-boot"), ("start_ticks", 44), ("state_dir", "/another-profile")):
wrong = dict(heartbeat, **{field: value})
background._write(sink.heartbeat_path, wrong)
self.assertFalse(self.status()["verified"], field)
sink.heartbeat_path.unlink()
self.assertFalse(self.status()["verified"])
def test_no_runtime_falls_back_to_bounded_fresh_durable_heartbeat(self):
(self.runtime_parent / str(os.getuid())).rmdir()
self.assertIsNone(background._heartbeat_directory(self.state, create=True))
clock = [100.0]
with mock.patch.object(background.time, "time", side_effect=lambda: clock[0]), mock.patch.object(background.time, "monotonic", side_effect=lambda: clock[0]), mock.patch.object(background, "_write", wraps=background._write) as write:
sink = background._StatusOutput(self.directory / "status.json", self.identity)
sink.observe("ready")
sink.change("ready", "[OK] Connected.")
write.reset_mock()
for instant in range(101, 121):
clock[0] = float(instant)
sink.observe("ready")
self.assertTrue(self.status()["verified"])
self.assertEqual(write.call_count, 4)
sink.observe("access")
self.assertFalse(self.status()["verified"])
def test_legacy_durable_heartbeat_remains_readable_with_runtime_available(self):
background._write(self.directory / "status.json", dict(self.identity, state="ready", verified_at=100.0))
with mock.patch.object(background.time, "time", return_value=110.0):
self.assertTrue(self.status()["verified"])
with mock.patch.object(background.time, "time", return_value=116.0):
self.assertFalse(self.status()["verified"])
def test_runtime_permissions_and_symlinks_rejected_without_changes(self):
root = self.runtime_parent / str(os.getuid())
root.chmod(0o755)
with self.assertRaises(RuntimeError):
background._heartbeat_directory(self.state, create=True)
self.assertEqual(root.stat().st_mode & 0o777, 0o755)
root.chmod(0o700)
directory = background._heartbeat_directory(self.state, create=True)
directory.rmdir()
directory.symlink_to(self.directory, target_is_directory=True)
with self.assertRaises(ValueError):
background._heartbeat_directory(self.state, create=True)
with self.assertRaises(RuntimeError):
background._heartbeat_directory(self.state)
def test_runtime_parent_symlink_rejected(self):
alias = self.root / "runtime-alias"
alias.symlink_to(self.runtime_parent, target_is_directory=True)
with mock.patch.object(background, "_RUNTIME_PARENT", alias), self.assertRaises(RuntimeError):
background._heartbeat_directory(self.state, create=True)
def test_heartbeat_file_symlink_and_hardlink_rejected(self):
sink = self.sink()
sink.heartbeat_path.symlink_to(sink.path)
with self.assertRaises(OSError):
sink.observe("ready")
sink.heartbeat_path.unlink()
os.link(sink.path, sink.heartbeat_path)
with self.assertRaises(RuntimeError):
sink.observe("ready")
def test_worker_state_only_writes_on_transition(self):
with mock.patch.object(autostart, "_json", wraps=autostart._json) as write:
autostart._worker_status(self.directory, "connected")
first = (self.directory / "worker-status.json").read_bytes()
for _ in range(20):
autostart._worker_status(self.directory, "connected")
self.assertEqual(write.call_count, 1)
self.assertEqual((self.directory / "worker-status.json").read_bytes(), first)
autostart._worker_status(self.directory, "reconnecting")
self.assertEqual(write.call_count, 2)
self.assertEqual(json.loads((self.directory / "worker-status.json").read_text())["state"], "reconnecting")
if __name__ == "__main__":
unittest.main()
+40
View File
@@ -1,10 +1,13 @@
"""Candidate profile/ABI selection; tests use inert native-file fixtures."""
import hashlib
import contextlib
import io
import json
import os
from pathlib import Path
import tempfile
import stat
import sys
from types import SimpleNamespace
import unittest
from unittest.mock import patch
@@ -64,6 +67,43 @@ class LinuxCompatibilityTests(unittest.TestCase):
self.assertFalse(self.check(distro, version, (3, 12))["ok"])
self.assertIsNone(compat.profile_for({"ID": "other", "ID_LIKE": "ubuntu", "VERSION_ID": "24.04"}))
def test_public_builder_profiles_are_accepted_by_the_runtime_without_claiming_validation(self):
# Use the real public builder's profile output, rather than duplicating
# its validation_status value in a fixture. Archive/vendor verification
# has separate tests; --check-vendor does not stage/install a package.
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
import archive_sources
import build_expanded
targets = [("debian13-amd64-py313", "debian", "13", (3, 13)),
("ubuntu24.04-amd64-py312", "ubuntu", "24.04", (3, 12)),
("mint22-amd64-py312", "linuxmint", "22.3", (3, 12))]
for profile_id, distro, version, python in targets:
with self.subTest(profile=profile_id):
self.artifact(profile_id)
output = io.StringIO()
globals_before = {name: getattr(build_expanded, name) for name in
("VERSION", "PUBLIC_RELEASE", "SOURCE_ARCHIVE", "PUBLIC_FILENAME", "RELEASE", "STAGE", "PREFIX")}
args = ["--target", profile_id, "--public-release", "--source-manifest", str(self.prefix / "source.json"), "--check-vendor"]
if profile_id != "debian13-amd64-py313":
args += ["--vendor-dir", str(self.prefix / "reviewed-cp312")]
with patch.multiple(build_expanded, **globals_before), \
patch.object(build_expanded.subprocess, "run"), \
patch.object(build_expanded, "vendor_versions", return_value={}), \
patch.object(build_expanded, "validate_vendor", return_value={"vendor_sha256": "a" * 64}), \
patch.object(build_expanded, "release_source", return_value={"filename": "FrameBridge-0.3.4-source.tar.gz", "sha256": "b" * 64}), \
patch.object(build_expanded, "legal_preflight"), \
patch.object(archive_sources, "source_files", return_value={}), \
contextlib.redirect_stdout(output):
build_expanded.main(args)
manifest = json.loads(output.getvalue())
self.assertEqual(manifest["validation_status"], "release_build")
(self.prefix / "release-profile.json").write_text(json.dumps(manifest))
result = self.check(distro, version, python)
self.assertTrue(result["ok"], result)
self.assertEqual(result["validation_status"], manifest["validation_status"])
self.assertIn("Release assembly does not establish a tested installation", result["warnings"][0])
self.assertNotEqual(result["validation_status"], "validated")
def test_interpreter_artifact_and_cpu_must_all_match(self):
self.artifact("ubuntu24.04-amd64-py312")
self.assertFalse(self.check("ubuntu", "24.04", (3, 13))["ok"])
+79
View File
@@ -0,0 +1,79 @@
"""Policy-name inspection only: no sudo, registry reads or rule contents."""
import os
from pathlib import Path
import stat
import tempfile
import unittest
from unittest import mock
from framebridge_expanded import autostart, credential_helper
class NodeHelperCleanupInspectionTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.state = self.root / "profile"
self.policies = self.root / "policies"
self.policies.mkdir()
self.profile = autostart._profile_id(self.state)
self.uid = str(os.getuid())
self.addCleanup(mock.patch.stopall)
mock.patch.object(autostart, "_helper_ready").start()
mock.patch.object(credential_helper, "SUDOERS", self.policies).start()
self.trust = mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path)
self.trust.start()
def test_own_canonical_decrypt_and_authorization_names_require_root_cleanup(self):
names = [
"framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64,
"framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-host",
"framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-user",
]
for name in names:
with self.subTest(name=name):
path = self.policies / name
path.touch()
with mock.patch.object(Path, "read_bytes", side_effect=AssertionError("Do not read rule contents")):
self.assertTrue(autostart._node_helper_cleanup_required(self.state))
path.unlink()
def test_ambiguous_names_require_review_even_alongside_a_canonical_grant(self):
canonical = "framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64
for name in ["framebridge-credential-" + self.uid + "-" + self.profile + "-invalid",
"framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-unknown"]:
with self.subTest(name=name), mock.patch.object(Path, "iterdir", return_value=iter([self.policies / canonical, self.policies / name])):
with self.assertRaisesRegex(RuntimeError, "ambiguous.*administrator review"):
autostart._node_helper_cleanup_required(self.state)
def test_symlink_in_own_namespace_still_requires_root_cleanup(self):
path = self.policies / ("framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64)
path.symlink_to(self.root / "nonexistent")
self.assertTrue(autostart._node_helper_cleanup_required(self.state))
def test_permission_denied_listing_is_not_treated_as_empty(self):
with mock.patch.object(Path, "iterdir", side_effect=PermissionError("synthetic denied")):
self.assertTrue(autostart._node_helper_cleanup_required(self.state))
def test_empty_verified_policy_directory_omits_privileged_cleanup(self):
self.assertFalse(autostart._node_helper_cleanup_required(self.state))
def test_insecure_parent_or_policy_directory_is_not_trusted(self):
self.trust.stop()
with mock.patch.object(credential_helper, "SUDOERS", Path("/etc/sudoers.d")):
for unsafe, kind, uid, mode in [
(Path("/etc"), stat.S_IFDIR, 0, 0o775),
(Path("/etc/sudoers.d"), stat.S_IFLNK, 0, 0o777),
(Path("/etc/sudoers.d"), stat.S_IFDIR, 12345, 0o755),
]:
def info(path, *, selected=unsafe, selected_kind=kind, selected_uid=uid, selected_mode=mode):
return os.stat_result((selected_kind | selected_mode if path == selected else stat.S_IFDIR | 0o755,
1, 1, 1, selected_uid if path == selected else 0, 0, 0, 0, 0, 0))
with self.subTest(path=unsafe, kind=kind, uid=uid, mode=mode), mock.patch.object(Path, "lstat", info), mock.patch.object(Path, "iterdir") as listing:
self.assertTrue(autostart._node_helper_cleanup_required(self.state))
listing.assert_not_called()
if __name__ == "__main__":
unittest.main()
+33 -1
View File
@@ -9,7 +9,7 @@ import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
from build_expanded import maintainer_prerm, PRIVILEGED_HELPER_FLOOR
from build_expanded import maintainer_preinst, maintainer_prerm, PRIVILEGED_HELPER_FLOOR
from build_profiles import CANDIDATE_VERSION
@@ -91,6 +91,38 @@ class MaintainerScriptTests(unittest.TestCase):
with self.assertRaises(ValueError):
maintainer_prerm("0.3.2'; /tmp/untrusted-script; '")
def run_preinst(self, arguments, *, version="0.3.4~dev.20261029.4", fail=0):
script = maintainer_preinst(version)
script = script.replace("/usr/bin/python3 -I -S - <<'FRAMEBRIDGE_UPGRADE_SNAPSHOT'",
shlex.quote(str(self.stub)) + " -I -S - <<'FRAMEBRIDGE_UPGRADE_SNAPSHOT'")
result = subprocess.run(["/bin/sh", "-s", "--", *arguments], input=script,
text=True, capture_output=True, timeout=10,
env=dict(os.environ, TEST_CALLS=str(self.trace), TEST_CLEANUP_RC=str(fail)))
calls = [json.loads(row) for row in self.trace.read_text().splitlines()] if self.trace.exists() else []
return result, calls
def test_same_version_reinstall_takes_a_new_current_runtime_snapshot(self):
result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.4"])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(calls, [["-I", "-S", "-"]])
def test_forward_upgrade_still_takes_a_runtime_snapshot(self):
result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.3"])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(calls, [["-I", "-S", "-"]])
def test_fresh_install_and_downgrade_do_not_capture_unrelated_running_profiles(self):
for arguments in (["install"], ["upgrade", "0.3.5"]):
with self.subTest(arguments=arguments):
result, calls = self.run_preinst(arguments)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(calls, [])
def test_failed_reinstall_snapshot_blocks_package_replacement(self):
result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.4"], fail=17)
self.assertEqual(result.returncode, 17)
self.assertEqual(calls, [["-I", "-S", "-"]])
if __name__ == "__main__":
unittest.main()
+52
View File
@@ -202,6 +202,58 @@ class PackageUpgradeTests(unittest.TestCase):
self.assertEqual(value['phase'], 'CAPTURED')
run.assert_not_called()
def test_reinstall_after_failure_preserves_receipt_and_keeps_stopped_profiles_stopped(self):
failed = {'schema': 1, 'transaction': 'a' * 16, 'phase': 'FAILED',
'profiles': [{'state': str(self.state), 'phase': 'FAILED',
'error': 'UPGRADE_OWNER_OPERATION_FAILED:user-ready'}]}
self.write(self.root / 'pending.json', failed)
processes = self.root / 'no-running-managers'
processes.mkdir()
marker = self.state / 'manual-stop.json'
marker.write_bytes(b'{"manual_stop": true}\n')
before_marker = marker.read_bytes()
with mock.patch.object(upgrade, 'ROOT', self.root), \
mock.patch.object(upgrade, '_root'), \
mock.patch.object(upgrade, '_read', side_effect=self.root_read), \
mock.patch.object(upgrade.subprocess, 'run') as run:
value = upgrade.snapshot(processes)
self.assertEqual(value['profiles'], [])
self.assertEqual(upgrade.schedule()['phase'], 'COMPLETE')
self.assertEqual(json.loads((self.root / 'previous.json').read_text()), failed)
self.assertEqual(marker.read_bytes(), before_marker)
run.assert_not_called()
def test_reinstall_after_failure_captures_only_current_managers_without_replaying_old_intent(self):
failed = {'schema': 1, 'phase': 'FAILED',
'profiles': [{'state': '/home/owner/previous-profile', 'phase': 'FAILED'}]}
self.write(self.root / 'pending.json', failed)
row = {'uid': 1000, 'state': str(self.state), 'pid': 1234, 'phase': 'CAPTURED'}
with mock.patch.object(upgrade, 'ROOT', self.root), \
mock.patch.object(upgrade, '_root'), \
mock.patch.object(upgrade, '_read', side_effect=self.root_read), \
mock.patch.object(upgrade, 'identify', return_value=row), \
mock.patch.object(upgrade.subprocess, 'run', return_value=SimpleNamespace(returncode=0)) as run:
value = upgrade.snapshot(self.root)
self.assertEqual(value['profiles'], [row])
self.assertEqual(upgrade.schedule()['phase'], 'SCHEDULED')
self.assertEqual(json.loads((self.root / 'previous.json').read_text()), failed)
self.assertEqual(run.call_count, 1)
self.assertEqual(run.call_args.args[0][-2:], [upgrade.MODULE, 'resume'])
def test_reinstall_does_not_replace_a_snapshot_with_a_live_upgrade_worker(self):
for phase in ('SCHEDULED', 'RUNNING'):
with self.subTest(phase=phase):
self.write(self.root / 'pending.json', {'schema': 1, 'phase': phase})
before = (self.root / 'pending.json').read_bytes()
with mock.patch.object(upgrade, 'ROOT', self.root), \
mock.patch.object(upgrade, '_root'), \
mock.patch.object(upgrade, '_read', side_effect=self.root_read), \
mock.patch.object(upgrade, 'identify', return_value=None):
with self.assertRaisesRegex(RuntimeError, 'PREVIOUS_WORKER_PENDING'):
upgrade.snapshot(self.root)
self.assertEqual((self.root / 'pending.json').read_bytes(), before)
self.assertFalse((self.root / 'previous.json').exists())
def test_optional_desktop_migration_failure_does_not_stop_runtime_recovery(self):
row = {'uid': 1000, 'user': 'owner', 'home': '/home/owner',
'state': str(self.state), 'pid': 1234, 'birth': '99',
@@ -0,0 +1,133 @@
"""Release assembly rejects stale or mismatched corresponding source."""
import hashlib
import io
import json
from pathlib import Path
import sys
import tarfile
import tempfile
import unittest
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
import build_expanded
from release_metadata import release_source
class PublicReleaseSourceTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.file = self.root / "module.py"
self.file.write_text("# reviewed release source\n")
self.archive = self.root / "FrameBridge-0.3.4-source.tar.gz"
self.vendor_paths = {
"cp313": "source/sources/original-geranas/FrameBridge-Fleet-Hub/vendor",
"cp312": "source/target-vendors/cp312",
}
vendor_data = b"# reviewed vendor input\n"
self.vendor_files = {"module.py": vendor_data,
"package/__init__.py": b"# package\n",
"package-1.dist-info/METADATA": b"Name: package\nVersion: 1\n"}
self.payloads = {"source/module.py": self.file.read_bytes(),
**{path + "/" + relative: data for path in self.vendor_paths.values()
for relative, data in self.vendor_files.items()}}
self.write_archive()
# The reviewed vendor format uses Path component ordering. In particular
# package/__init__.py comes before package-1.dist-info/METADATA.
digest = hashlib.sha256()
for relative in ("module.py", "package/__init__.py", "package-1.dist-info/METADATA"):
digest.update(relative.encode() + b"\0")
digest.update(hashlib.sha256(self.vendor_files[relative]).digest())
self.vendor = {"vendor_sha256": digest.hexdigest()}
self.data = {
"version": "0.3.4", "status": "RELEASE_SOURCE_ASSEMBLED",
"archive": self.archive.name, "bytes": self.archive.stat().st_size,
"sha256": hashlib.sha256(self.archive.read_bytes()).hexdigest(),
"vendor_inputs": {"vendors": {abi: {**self.vendor, "path": path} for abi, path in self.vendor_paths.items()}},
"files": [{"path": path, "sha256": hashlib.sha256(data).hexdigest()} for path, data in self.payloads.items()],
}
self.manifest = self.root / "SOURCE-ARCHIVE-MANIFEST.json"
def write_archive(self):
with tarfile.open(self.archive, "w:gz") as archive:
for path, data in self.payloads.items():
info = tarfile.TarInfo("FrameBridge-0.3.4/" + path)
info.size = len(data)
archive.addfile(info, io.BytesIO(data))
def check(self, profile="debian13-amd64-py313", version="0.3.4"):
self.manifest.write_text(json.dumps(self.data))
return release_source(self.manifest, version, profile, {"source/module.py": self.file}, self.vendor)
def test_release_binds_archive_source_and_both_vendor_abis(self):
for profile in ("debian13-amd64-py313", "ubuntu24.04-amd64-py312", "mint22-amd64-py312"):
self.assertEqual(self.check(profile)["sha256"], self.data["sha256"])
def test_source_changed_after_archive_is_rejected(self):
self.file.write_text("# later edit\n")
with self.assertRaisesRegex(RuntimeError, "Package source differs"):
self.check()
def test_archive_tamper_is_rejected(self):
with self.archive.open("ab") as stream:
stream.write(b"altered")
with self.assertRaisesRegex(RuntimeError, "archive hash or size"):
self.check()
def test_changed_manifest_cannot_relabel_old_archive_contents(self):
self.file.write_text("# later edit\n")
self.data["files"][0]["sha256"] = hashlib.sha256(self.file.read_bytes()).hexdigest()
with self.assertRaisesRegex(RuntimeError, "archive content differs"):
self.check()
def test_relabelled_alternate_vendor_bytes_are_rejected_even_by_debian_build(self):
self.payloads[self.vendor_paths["cp312"] + "/module.py"] = b"# different vendor bytes\n"
self.write_archive()
self.data.update(bytes=self.archive.stat().st_size, sha256=hashlib.sha256(self.archive.read_bytes()).hexdigest())
self.data["files"] = [{"path": path, "sha256": hashlib.sha256(data).hexdigest()} for path, data in self.payloads.items()]
with self.assertRaisesRegex(RuntimeError, "Archived vendor bytes differ"):
self.check()
def test_unsafe_archive_member_is_rejected_without_extracting(self):
self.payloads["../escape"] = b"unwanted path"
self.write_archive()
self.data.update(bytes=self.archive.stat().st_size, sha256=hashlib.sha256(self.archive.read_bytes()).hexdigest())
self.data["files"].append({"path": "../escape", "sha256": hashlib.sha256(b"unwanted path").hexdigest()})
with self.assertRaisesRegex(RuntimeError, "Unsafe release source archive path"):
self.check()
def test_release_requires_cp312_corresponding_source(self):
del self.data["vendor_inputs"]["vendors"]["cp312"]
with self.assertRaisesRegex(RuntimeError, "cp312 and cp313"):
self.check()
def test_vendor_input_cannot_differ_from_corresponding_source(self):
self.data["vendor_inputs"]["vendors"]["cp312"]["vendor_sha256"] = "b" * 64
with self.assertRaisesRegex(RuntimeError, "vendor input differs"):
self.check("mint22-amd64-py312")
def test_candidate_version_and_experimental_profile_are_not_public_release(self):
for profile, version in (("lmde7-amd64-py313", "0.3.4"), ("debian13-amd64-py313", "0.3.4~dev.1")):
with self.assertRaisesRegex(RuntimeError, "stable version and an official"):
self.check(profile, version)
def test_public_metadata_names_source_without_claiming_signature_or_vm_qa(self):
docs = self.root / "docs"
docs.mkdir()
identity = self.check()
with mock.patch.object(build_expanded, "PUBLIC_RELEASE", True), \
mock.patch.object(build_expanded, "SOURCE_ARCHIVE", identity), \
mock.patch.object(build_expanded, "VERSION", "0.3.4"):
build_expanded.install_legal_material(docs)
result = json.loads((docs / "COMMUNITY-RELEASE-STATUS.json").read_text())
self.assertEqual(result["status"], "PUBLIC_RELEASE_BUILD_ASSEMBLED")
self.assertEqual(result["source_archive"], identity)
self.assertEqual(result["signature_status"], "EXTERNAL_RELEASE_RECORD")
self.assertEqual(result["vm_lifecycle_validation"], "NOT_ASSERTED_BY_BUILDER")
self.assertNotIn("compatibility candidate", (docs / "copyright").read_text())
if __name__ == "__main__":
unittest.main()