From 6295f0fce1edc677ae96f184280add955336e608 Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 10 May 2026 19:28:16 -0700 Subject: [PATCH] feat: add utility function to restore bytes from JSON and enhance prefs handling in RepeaterCompanionBridge - Introduced `_prefs_bytes_from_json` to convert JSON hex strings back to bytes for NodePrefs fields. - Updated `_load_prefs` logic to handle bytes conversion when loading preferences. - Improved the handling of preferences in the `RepeaterCompanionBridge` class to ensure proper type restoration. --- repeater/companion/bridge.py | 23 ++++++++++-- tests/test_companion_bridge_prefs.py | 54 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/test_companion_bridge_prefs.py diff --git a/repeater/companion/bridge.py b/repeater/companion/bridge.py index ac09b6f..d33003d 100644 --- a/repeater/companion/bridge.py +++ b/repeater/companion/bridge.py @@ -18,6 +18,24 @@ logger = logging.getLogger("RepeaterCompanionBridge") +def _prefs_bytes_from_json(value: Any) -> bytes: + """Restore a ``bytes`` NodePrefs field from JSON (hex string from :func:`_to_json_safe`).""" + if value is None: + return b"" + if isinstance(value, (bytes, bytearray)): + return bytes(value) + if isinstance(value, str): + s = value.strip() + if not s: + return b"" + try: + return bytes.fromhex(s) + except ValueError: + logger.debug("Invalid hex for prefs bytes field (prefix %r)", s[:32]) + return b"" + return b"" + + def _to_json_safe(value: Any) -> Any: """Convert a value to a JSON-serializable form (avoids TypeError from enums, bytes, etc.).""" if value is None or isinstance(value, (bool, int, float, str)): @@ -70,8 +88,6 @@ def __init__( authenticate_callback=authenticate_callback, initial_contacts=initial_contacts, ) - # Load persisted prefs (e.g. node_name) from SQLite so matching uses last-saved name - self._load_prefs() def _save_prefs(self) -> None: """Persist full NodePrefs as JSON to SQLite.""" @@ -104,6 +120,9 @@ def _load_prefs(self) -> None: try: if value is None: continue + if isinstance(current, bytes): + setattr(self.prefs, key, _prefs_bytes_from_json(value)) + continue if isinstance(current, bool): setattr(self.prefs, key, bool(value)) elif isinstance(current, int): diff --git a/tests/test_companion_bridge_prefs.py b/tests/test_companion_bridge_prefs.py new file mode 100644 index 0000000..cfcc5d1 --- /dev/null +++ b/tests/test_companion_bridge_prefs.py @@ -0,0 +1,54 @@ +"""Tests for RepeaterCompanionBridge prefs JSON round-trip (bytes fields).""" + +import pytest + +from pymc_core import LocalIdentity + +from repeater.companion.bridge import RepeaterCompanionBridge, _prefs_bytes_from_json + + +@pytest.fixture +def identity(): + return LocalIdentity() + + +def test_prefs_bytes_from_json_round_trip(): + assert _prefs_bytes_from_json("") == b"" + assert _prefs_bytes_from_json("00") == b"\x00" + key = bytes(range(16)) + assert _prefs_bytes_from_json(key.hex()) == key + assert _prefs_bytes_from_json(bytearray(key)) == key + assert _prefs_bytes_from_json(key) == key + assert _prefs_bytes_from_json("not-hex") == b"" + + +def test_load_prefs_restores_default_scope_key_as_bytes(identity): + """Hex strings from SQLite JSON must become bytes (not str) on NodePrefs.""" + + class FakeSqlite: + def companion_load_prefs(self, companion_hash: str): + return { + "default_scope_name": "region1", + "default_scope_key": bytes(range(16)).hex(), + } + + def companion_save_prefs(self, companion_hash: str, prefs: dict) -> bool: + return True + + async def inject(pkt, wait_for_ack=False): + return True + + bridge = RepeaterCompanionBridge( + identity, + inject, + sqlite_handler=FakeSqlite(), + companion_hash="testhash", + node_name="bootname", + ) + assert bridge.prefs.default_scope_name == "region1" + assert isinstance(bridge.prefs.default_scope_key, bytes) + assert bridge.prefs.default_scope_key == bytes(range(16)) + scope = bridge.get_default_flood_scope() + assert scope is not None + assert scope[0] == "region1" + assert scope[1] == bytes(range(16))