diff --git a/Tests/interop/README.md b/Tests/interop/README.md index 5882adc..3bcd8d3 100644 --- a/Tests/interop/README.md +++ b/Tests/interop/README.md @@ -1,89 +1,146 @@ # Columba-iOS interop test harness -Hermetic end-to-end interop suite for Columba-iOS, modelled on Columba-Android's `tests/interop/`. Pins iOS-side wire-format correctness against a reference Sideband peer running headless on the same host. +Hermetic end-to-end interop suite for Columba-iOS, modelled on Columba-Android's +`tests/interop/`. Pins iOS-side wire-format correctness against a reference +Sideband peer running headless on the same host. The pytest suite +(`test_attachments.py`, `test_telemetry.py`) drives the iOS Columba simulator +via Maestro / `lxma://` deep links and asserts delivery on the Sideband side +(and the reverse). 14 cases: text / image (opportunistic, direct, propagated) / +file, both directions, plus location telemetry, custom-meta, periodic-share +toggle, and cease. ## Topology ``` -┌─────────────────────────────────┐ ┌──────────────────────────────┐ -│ iOS Simulator (iPhone 17 …) │ │ Sideband-headless peer │ -│ Columba.app │ │ sbapp.sideband.core (in- │ -│ identity: 7030e48e… │ │ process via peer_sideband) │ -│ TCPClient → 127.0.0.1:4242 │ │ identity: 95617c21… │ -│ ↑ │ │ ↑ (RNS shared instance) │ -└───┼──────────────────────────────┘ └───┼──────────────────────────┘ +┌──────────────────────────────────┐ ┌──────────────────────────────┐ +│ iOS Simulator (iPhone 17) │ │ Sideband-headless peer │ +│ Columba.app │ │ sbapp.sideband.core (in- │ +│ TCPClient → 127.0.0.1:4242 │ │ process via peer_sideband) │ +│ (auto-seeded by conftest) │ │ ↑ shared instance :37428 │ +└───┼──────────────────────────────┘ └───┼──────────────────────────┘ │ │ - └─────────────┐ ┌──────────────────┘ - ▼ ▼ - ┌───────────────────┐ - │ rnsd (Mac) │ - │ TCPServer :4242 │ - │ share_instance Y │ - └───────────────────┘ + │ TCP :4242 LocalServer │ :37428 + └──────────────┐ ┌─────────────────┘ + ▼ ▼ + ┌─────────────────────────────┐ + │ lxmd (network.lxmf.lxmd) │ ← owns the shared RNS instance + │ "Columba Hub" prop node │ enable_transport = True + │ TCPServer :4242 │ share_instance = Yes + │ LocalServer :37428 │ + └─────────────────────────────┘ + ▲ ▲ + │ (also clients of the shared instance) + rnsd nomadnet ``` -All three RNS endpoints share the host transport (rnsd bridges the simulator's TCPClient to Sideband's shared-instance socket). No physical phone required. +**`lxmd` — not rnsd — owns the shared RNS instance + the `:4242` TCPServer.** +rnsd and nomadnet run alongside it as shared-instance *clients*. lxmd is the +transport node (`enable_transport = True`) that bridges the sim's TCPClient to +Sideband's shared-instance connection, so an announce/message from one side +reaches the other. No physical phone required. ## Components -- **`peer_sideband.py`** — `SidebandPeer`, the in-process Sideband daemon wrapper. Copied from Columba-Android's `tests/interop/peer_sideband.py` (commit `cf18d31`). Provides `start()`, `identity_hex`, `send_text()`, `wait_for_message()`, and a `wait_for_tapped_message()` that sees every inbound `LXMessage` *before* Sideband filters it — the diagnostic surface for "iOS shows delivered but message disappears" bugs (the proof fires at the RNS layer before any LXMF processing — see `LXMRouter.delivery_packet:1822`). -- **`run_peer.py`** — Standalone runner: `python run_peer.py` to boot the peer, watch the tap, and print every inbound `LXMessage` (source, content, signature_validated, field-key set). Used interactively while debugging; the pytest harness will drive `SidebandPeer` directly. -- **`flows/`** — Maestro YAML flows for driving the iOS Columba simulator UI. -- **`screenshots/`** — Where Maestro test runs write `takeScreenshot:` outputs. Gitignored. +- **`conftest.py`** — pytest fixtures. Session-scoped, self-contained: + - `sideband` — boots the in-process `SidebandPeer` once per session. + - `_ensure_sim_interface` — **auto-seeds** a TCP-client interface + (`127.0.0.1:4242`) into Columba's interface store and relaunches, so a + fresh sim install (which comes up with 0 interfaces) bridges to lxmd with + no manual "Settings → Add Interface" step. Override host/port with + `RNSD_TCP_HOST` / `RNSD_TCP_PORT`. + - `_bootstrap_paths` — cold-starts the bidirectional path table (each side + must have heard the other's announce), then pins lxmd's propagation node + on the sim (discovered via `lxmd --status`) so the propagated test runs + deterministically instead of skipping on lxmd's 5-min announce cadence. + - `Simulator` helper — reads the app's `Documents/diag.log` (the + `[RNS] started/inbound/announce/delivery` lines — note the backend-agnostic + `[RNS]` prefix, renamed from `[PY]` in the dual-backend refactor), fires + `lxma://` deep links via Maestro, and resolves the sim's identity / LXMF + delivery hash. +- **`peer_sideband.py`** — `SidebandPeer`, the in-process Sideband daemon + wrapper. Provides `start()`, `identity_hex`, `send_text()`, + `wait_for_message()`, and `wait_for_tapped_message()` that sees every inbound + `LXMessage` *before* Sideband filters it — the diagnostic surface for "iOS + shows delivered but message disappears" bugs (fires at the RNS layer before + any LXMF processing). Teardown uses `SidebandCore.cleanup()`. +- **`run_peer.py`** — Standalone runner (`python run_peer.py`) to boot the peer + interactively and print every inbound `LXMessage`. Used while debugging; the + pytest suite drives `SidebandPeer` directly. +- **`flows/`** — Maestro YAML flows that drive the Columba simulator UI. +- **`screenshots/`** — Maestro `takeScreenshot:` outputs (gitignored). ## Prerequisites -- macOS with Xcode 26+ and an iOS 26 simulator booted -- `rnsd` running with a TCPServer on `127.0.0.1:4242` and `share_instance = Yes` -- Sideband checkout at `~/repos/Sideband` -- Python venv with the RNS / LXMF wheels installed (the same venv `rnsd` uses is fine — `~/.reticulum-host/venv`) -- `maestro` installed (`brew install maestro`) -- Columba.app built for the simulator (see "Build" below) - -## Build +- macOS with Xcode 26+ and **one** iOS simulator booted (iPhone 17). +- **`lxmd` running** as the shared instance: `enable_transport = True`, + `share_instance = Yes`, and a `[[TCP Server]]` on `127.0.0.1:4242` + (config in `~/.lxmd/config` + `~/.reticulum/config`). rnsd/nomadnet may run + alongside as clients but lxmd is what the sim and Sideband bridge through. +- Sideband checkout at `~/repos/Sideband` (override with `SIDEBAND_SRC`). +- A Python env with `RNS` / `LXMF` / `pytest` (the host venv + `~/.reticulum-host/venv` works). +- `maestro` installed (`brew install maestro`); JDK 21 on `JAVA_HOME` for it. +- Columba.app **installed** on the booted sim (built `Debug` so the `lxma://` + test deep links — which are `#if DEBUG`-gated in production — are present). + The TCP interface to lxmd is seeded automatically by the suite; you do **not** + need to configure it by hand. + +## Build + install the app ```bash cd ~/repos/Columba-iOS xcrun simctl boot "iPhone 17" -xcodebuild -project Columba.xcodeproj -scheme Columba -configuration Debug \ +xcodebuild -scheme Columba -configuration Debug \ -destination "platform=iOS Simulator,name=iPhone 17" \ -derivedDataPath /tmp/columba-sim-build build - -xcrun simctl install booted /tmp/columba-sim-build/Build/Products/Debug-iphonesimulator/ColumbaApp.app +xcrun simctl install booted \ + /tmp/columba-sim-build/Build/Products/Debug-iphonesimulator/ColumbaApp.app ``` -## Run a manual diagnostic - -In one shell, boot the Sideband peer: +## Run the suite ```bash -~/.reticulum-host/venv/bin/python tests/interop/run_peer.py --watch -# prints identity_hex; leave running +cd ~/repos/Columba-iOS/Tests/interop +export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home +python3 -m pytest -v ``` -In another shell, launch Columba on the simulator with the TCP-hub env so it -joins the same rnsd: +The session fixtures handle everything else: seed the sim's lxmd interface, +boot the Sideband peer, bootstrap paths, pin the propagation node. Expect +**14 passed**. (A run takes ~5 min — Sideband boot + a Maestro flow / deep-link +send + delivery wait per case.) + +## Manual diagnostic ```bash -SIMCTL_CHILD_COLUMBA_TCP_HUB=127.0.0.1:4242 \ - xcrun simctl launch booted network.columba.Columba +SIDEBAND_SRC=~/repos/Sideband python3 run_peer.py # prints identity_hex, watches the tap ``` - -Drive a send via Maestro: - +Then drive a flow against the booted sim: ```bash maestro --device "$(xcrun simctl list devices booted | awk '/Booted/{print $NF}' | tr -d '()')" \ - test tests/interop/flows/send_text_to_sideband.yaml + test flows/send_text_to_peer.yaml ``` - -Watch the peer log — `tap` prints every inbound `LXMessage` with its `signature_validated` flag and the field-key set. If iOS Columba reports "delivered" but no tap entry appears for the corresponding text, the bug is on the iOS send wire. If the tap entry shows `signature_validated=False`, iOS's announce isn't reaching Sideband. If the tap entry has the right content but Sideband's DB doesn't, the issue is downstream of the upstream LXMF callback. - -## Future: pytest-driven runs - -Like Columba-Android's suite, individual interop tests will be pytest cases that: -1. Spawn `SidebandPeer`, -2. Drive Maestro to send from iOS, -3. Assert on the tap (and/or the Sideband DB), -4. Reverse direction: `peer.send_text(ios_hash, …)` + Maestro screenshot/assertion that the bubble appeared on iOS. - -The `screenshots/` directory captures the per-step UI state for triage when an assertion fails. +The tap prints every inbound `LXMessage` with its `signature_validated` flag and +field-key set. If Columba reports "delivered" but no tap entry appears, the bug +is on the iOS send wire; if the tap shows `signature_validated=False`, iOS's +announce isn't reaching Sideband; if the content is right but Sideband's DB +isn't, the issue is downstream of the upstream LXMF callback. + +## Troubleshooting + +- **Every case errors at setup with "sim never heard the peer announce"** (and + more broadly, devices connect but receive nothing — `rx=0` on their links): + **lxmd is wedged.** It accepts TCP connections but stops routing packets + (symptom: `~/.reticulum/logfile` stops growing, lxmd at 0% CPU). This also + takes down real phones bridging through it. Fix — bounce it: + ```bash + launchctl kickstart -k gui/$(id -u)/network.lxmf.lxmd + ``` + Confirm it recovered: `lxmd --status` shows peers/traffic and the logfile + resumes `Valid announce for …` lines. +- **`test_image_..._propagated` skips:** the suite pins lxmd's prop node via + `lxmd --status`; a skip means lxmd was unreachable at bootstrap. Check lxmd + is up and `--status` works. +- **Suite can't read the sim identity / "Couldn't find `[RNS] started…`":** + the app must be a `Debug` build (so the deep-link handlers exist) and must + have launched at least once (it clears + writes `diag.log` on launch). diff --git a/Tests/interop/conftest.py b/Tests/interop/conftest.py index 2fac7fc..6968ba2 100644 --- a/Tests/interop/conftest.py +++ b/Tests/interop/conftest.py @@ -25,7 +25,9 @@ """ from __future__ import annotations +import json import os +import plistlib import re import shutil import subprocess @@ -46,9 +48,23 @@ sys.path.insert(0, SIDEBAND_SRC) BUNDLE_ID = "network.columba.Columba" +APP_GROUP_ID = "group.network.columba.Columba" # InterfaceRepository's UserDefaults suite PROP_NODE_HEX = os.environ.get("PROP_NODE_HEX", "") # e.g. lxmd's hash; auto-detected at session start LOG_TAIL_LINES = 800 # how much of Documents/diag.log we keep handy per assertion +# The host rnsd/lxmd shared instance the interop sim bridges through. The suite +# seeds a TCP-client interface to this into Columba's interface store at session +# start (see _ensure_rnsd_interface), so a fresh install / clean sim needs no +# manual Settings → Network Interfaces → Add step. Override for a non-default host. +RNSD_TCP_HOST = os.environ.get("RNSD_TCP_HOST", "127.0.0.1") +RNSD_TCP_PORT = int(os.environ.get("RNSD_TCP_PORT", "4242")) + +# lxmd binary — used to read the propagation-node hash (--status) and to force +# a propagation announce during bootstrap (so the propagated test runs instead +# of skipping on lxmd's 5-min announce cadence). Override with LXMD_BIN. +LXMD_BIN = (os.environ.get("LXMD_BIN") or shutil.which("lxmd") + or os.path.expanduser("~/.reticulum-host/venv/bin/lxmd")) + # ── helpers ────────────────────────────────────────────────────────────── @@ -95,6 +111,102 @@ def _app_data_container(udid: str) -> Path: return Path(out.strip()) +def _app_group_prefs_plist(udid: str) -> Optional[Path]: + """The app-group UserDefaults plist where InterfaceRepository persists once + the app-group suite has been created. None on a fresh sim that hasn't + written it yet (the app falls back to / migrates from .standard).""" + base = (Path.home() / "Library/Developer/CoreSimulator/Devices" / udid + / "data/Containers/Shared/AppGroup") + for p in base.glob(f"*/Library/Preferences/{APP_GROUP_ID}.plist"): + return p + return None + + +def _ensure_rnsd_interface(udid: str) -> None: + """Make the suite self-contained: seed Columba's interface store with a + single TCP-client interface to the host rnsd/lxmd shared instance + (RNSD_TCP_HOST:RNSD_TCP_PORT) so the sim bridges to the Sideband peer — + no manual "Add Interface" step on a fresh install / clean sim. + + InterfaceRepository persists interfaces as a JSON blob under + `com.columba.interfaces` in UserDefaults (app-group suite, falling back to + / migrated from .standard). We write the blob straight into the container + plist(s): `simctl spawn defaults` targets the booted-user GLOBAL domain, + NOT the sandbox/app-group plist the app actually reads. Then bounce cfprefsd + so the relaunched app re-reads our edit instead of flushing a stale copy + back. Deterministic + idempotent — the store is set to exactly this one + interface (a dedicated interop sim wants nothing else).""" + entity = [{ + "id": "interop-rnsd-tcp", "name": "rnsd-interop", "type": "TCPClient", + "enabled": True, "mode": "full", + "config": {"type": "tcpClient", + "config": {"targetHost": RNSD_TCP_HOST, "targetPort": RNSD_TCP_PORT}}, + "displayOrder": 0, "createdAt": 0, "updatedAt": 0, + }] + blob = json.dumps(entity).encode("utf-8") + + subprocess.run(["xcrun", "simctl", "terminate", udid, BUNDLE_ID], capture_output=True) + + # Write to the standard sandbox plist AND the app-group plist if it exists. + # First run: only standard exists → the app's migrateFromStandardDefaults + # copies it into the app-group suite on launch. Later runs: app-group is + # authoritative (migration is skipped once it has the key), so we must set + # it directly too. + targets = [_app_data_container(udid) / "Library/Preferences" / f"{BUNDLE_ID}.plist"] + ag = _app_group_prefs_plist(udid) + if ag is not None: + targets.append(ag) + for plist_path in targets: + try: + with open(plist_path, "rb") as f: + pl = plistlib.load(f) + except Exception: + pl = {} + pl["com.columba.interfaces"] = blob # bytes -> , exactly UserDefaults.set(Data) + plist_path.parent.mkdir(parents=True, exist_ok=True) + with open(plist_path, "wb") as f: + plistlib.dump(pl, f, fmt=plistlib.FMT_BINARY) + + # cfprefsd caches container prefs; kick it AFTER editing so it re-reads our + # file rather than serving / flushing back the stale in-memory copy + # (mirrors clean_location_state's dance). user/501 is the sim's only scope. + subprocess.run(["xcrun", "simctl", "spawn", udid, "launchctl", "kickstart", "-k", + "user/501/com.apple.cfprefsd.xpc.daemon"], capture_output=True) + time.sleep(0.5) + subprocess.run(["xcrun", "simctl", "launch", udid, BUNDLE_ID], check=True, capture_output=True) + + # Confirm the backend loaded the interface (config written with N>=1), so a + # silent seed failure surfaces here rather than as a confusing path-bootstrap + # timeout. diag.log is cleared on launch, so the match is fresh. + diag = _app_data_container(udid) / "Documents" / "diag.log" + deadline = time.time() + 45.0 + while time.time() < deadline: + try: + text = diag.read_text(errors="replace") + except FileNotFoundError: + text = "" + if re.search(r"\[RNS\] wrote config \(\d+ bytes, ([1-9]\d*) interfaces\)", text): + return + time.sleep(0.5) + pytest.fail( + f"Columba did not register the rnsd TCP interface " + f"({RNSD_TCP_HOST}:{RNSD_TCP_PORT}) within 45 s of relaunch — the interface " + f"seed did not take (check the sandbox/app-group prefs plist)." + ) + + +def _lxmd_propagation_hex() -> Optional[str]: + """Query the running lxmd for its LXMF propagation-node destination hash + (the `--status` line `Propagation Node running on `). None if lxmd is + down / unreachable.""" + try: + out = _sh([LXMD_BIN, "--status", "--timeout", "10"], check=False, timeout=20) + except Exception: + return None + m = re.search(r"Propagation Node running on <([0-9a-f]+)>", out) + return m.group(1) if m else None + + # ── data classes ────────────────────────────────────────────────────────── @@ -140,15 +252,15 @@ def __init__(self, udid: str): @property def identity_hex(self) -> str: - """Local RNS identity hash hex (the `[PY] started identity=…` line).""" + """Local RNS identity hash hex (the `[RNS] started identity=…` line).""" if self._cached_identity_hex is not None: return self._cached_identity_hex for line in self._tail_diag(LOG_TAIL_LINES * 4): - m = re.search(r"\[PY\] started identity=([0-9a-f]+)\s+destination=", line) + m = re.search(r"\[RNS\] started identity=([0-9a-f]+)\s+destination=", line) if m: self._cached_identity_hex = m.group(1) return self._cached_identity_hex - pytest.fail("Couldn't find `[PY] started identity=…` in diag.log") + pytest.fail("Couldn't find `[RNS] started identity=…` in diag.log") @property def lxmf_delivery_hex(self) -> str: @@ -156,7 +268,7 @@ def lxmf_delivery_hex(self) -> str: if self._cached_lxmf_delivery_hex is not None: return self._cached_lxmf_delivery_hex for line in self._tail_diag(LOG_TAIL_LINES * 4): - m = re.search(r"\[PY\] started identity=[0-9a-f]+\s+destination=([0-9a-f]+)", line) + m = re.search(r"\[RNS\] started identity=[0-9a-f]+\s+destination=([0-9a-f]+)", line) if m: self._cached_lxmf_delivery_hex = m.group(1) return self._cached_lxmf_delivery_hex @@ -182,7 +294,7 @@ def auto_propagation_node_hex(self) -> Optional[str]: return self._cached_propagation_node_hex for line in reversed(self._tail_diag(LOG_TAIL_LINES * 4)): m = re.search( - r"\[PY\] announce dest=([0-9a-f]+)\s+aspect=lxmf\.propagation\s+name=\"([^\"]*)\"", + r"\[RNS\] announce dest=([0-9a-f]+)\s+aspect=lxmf\.propagation\s+name=\"([^\"]*)\"", line, ) if m and m.group(2): # non-empty name @@ -324,7 +436,7 @@ def assert_bubble_visible( Pins the *render* half of the inbound path: a bubble with an empty content, no image, and no file would still be persisted (and would - let the diag.log `[PY] inbound` proxy assertion pass), but this + let the diag.log `[RNS] inbound` proxy assertion pass), but this catches a regression in `MessageBubble.init(from record:)` / `LxmfFieldCodec.unpack(...)` / the SwiftUI rendering of attachment payloads. @@ -412,7 +524,7 @@ def assert_peer_pin_visible(self, *, timeout: float = 40.0) -> None: # would empty it and also fire AppServices.initialize → # DiagLog.clear, wiping the [LOC-RECV] line. `stopApp: false` # just activates the already-running process — verified to leave - # the [PY] started count and diag.log untouched. The app can end + # the [RNS] started count and diag.log untouched. The app can end # up backgrounded after the bootstrap fixture's `simctl openurl` # announces, so don't assume it's already frontmost. "- launchApp: { stopApp: false }", @@ -529,7 +641,16 @@ def sideband(): @pytest.fixture(scope="session", autouse=True) -def _bootstrap_paths(sim, sideband): +def _ensure_sim_interface(sim): + """Seed the sim's TCP-client interface to the host rnsd/lxmd shared instance + before anything else, so the regression suite is fully self-contained (no + manual 'Add Interface' setup on a clean sim). _bootstrap_paths depends on + this so it always runs first and the relaunched app is bridged in.""" + _ensure_rnsd_interface(sim.udid) + + +@pytest.fixture(scope="session", autouse=True) +def _bootstrap_paths(sim, sideband, _ensure_sim_interface): """Cold-start the bidirectional path table before the first test. Each direction needs the *other side* to know our identity: @@ -595,6 +716,29 @@ def _bootstrap_paths(sim, sideband): f"announce isn't reaching rnsd's shared-instance side." ) + # Make lxmd's propagation node deterministically known to the sim now that + # sim + Sideband are connected, so test_image_…_propagated runs instead of + # skipping on lxmd's 5-min announce cadence (the ordering flake). + # + # We can't force a true lxmd broadcast announce non-disruptively: lxmd has + # no announce trigger (CLI/signal), and RNS.Transport.request_path here is a + # no-op because this shared-instance process already holds the path so no + # request leaves the host. A real re-announce needs an lxmd restart, which + # would drop every mesh device (incl. the physical phones) — wrong for a + # regression run. Instead discover lxmd's prop-node hash from `--status` and + # inject it into the sim helper's cache, so auto_propagation_node_hex + # resolves it. The propagated test still exercises the real delivery path + # (sim → lxmd message store → Sideband sync); only the announce-discovery + # hop is shortcut. Warn (don't fail) if lxmd is unreachable. + prop_hex = PROP_NODE_HEX or _lxmd_propagation_hex() + if prop_hex: + sim._cached_propagation_node_hex = prop_hex + print(f"[BOOT] propagation node pinned to {prop_hex[:12]}… (via lxmd --status)", + flush=True) + else: + print("[BOOT] WARNING: lxmd propagation hash unavailable (lxmd down?); " + "propagated test will skip.", flush=True) + @pytest.fixture def clean_location_state(sim): @@ -676,7 +820,7 @@ def clean_location_state(sim): ) # AppServices.initialize() runs on every launch: it clears diag.log # (DiagLog.clear) very early, then the Python backend logs - # `[PY] started identity=…` ~6–8 s in on a warm sim and longer on a cold + # `[RNS] started identity=…` ~6–8 s in on a warm sim and longer on a cold # sim / slow CI runner. Poll for that readiness line instead of a flat # 8 s sleep — a fixed sleep that under-shoots hands control to the test # before the `lxma://` deep-link handler is registered, so the @@ -684,7 +828,7 @@ def clean_location_state(sim): # `wait_for_tapped_message` with no hint the app simply wasn't ready. # Gate on the clear first (current size drops below the pre-launch log, # with a short fallback since the clear is a truncate at init start) so we - # don't match the *previous* session's `[PY] started` line still on disk. + # don't match the *previous* session's `[RNS] started` line still on disk. pre_size = sim.diag_log.stat().st_size if sim.diag_log.exists() else 0 cleared = pre_size == 0 clear_fallback = time.time() + 3.0 @@ -694,13 +838,13 @@ def clean_location_state(sim): if not cleared and (size < pre_size or time.time() > clear_fallback): cleared = True if cleared and any( - "[PY] started identity=" in line for line in sim._tail_diag(LOG_TAIL_LINES) + "[RNS] started identity=" in line for line in sim._tail_diag(LOG_TAIL_LINES) ): break time.sleep(0.5) else: pytest.fail( - "Columba did not log `[PY] started identity=…` within 45 s of " + "Columba did not log `[RNS] started identity=…` within 45 s of " "relaunch — AppServices.initialize is stuck or slower than expected, " "so the location-toggle deep link would no-op against an unready app." ) diff --git a/Tests/interop/peer_sideband.py b/Tests/interop/peer_sideband.py index af8e703..fd47090 100644 --- a/Tests/interop/peer_sideband.py +++ b/Tests/interop/peer_sideband.py @@ -255,23 +255,25 @@ def stop(self) -> None: process exit to reap RNS threads. CI runners should treat each pytest session as the unit of teardown.""" if self._core is not None: - # SidebandCore has a `cleanup()`-ish method? — looking at the - # source it's `__exit_handler` registered via atexit. Calling - # it manually here is safe and ensures DB flushes. Guard the - # private name explicitly (like _attach_inbound_tap does for - # __delivery_callback) so a Sideband rename surfaces loudly - # instead of silently leaking RNS threads between sessions. - if hasattr(self._core, "_exit_handler"): + # SidebandCore's teardown is `cleanup()` (the older private + # `__exit_handler` was removed). On the host it connects to the + # shared rnsd/lxmd instance, so cleanup() is intentionally a near + # no-op (RNS teardown belongs to the shared instance, not us); we + # still call it for DB flush + future-proofing, and rely on process + # exit to reap threads. Guard the name (like _attach_inbound_tap + # does for __delivery_callback) so a future Sideband rename surfaces + # loudly instead of silently skipping teardown. + if hasattr(self._core, "cleanup"): try: - self._core._exit_handler() + self._core.cleanup() except Exception: # noqa: BLE001 # Sideband logs its own teardown errors; suppress here. pass else: print( - "[peer_sideband] WARNING: SidebandCore._exit_handler is gone " - "— Sideband may have renamed it; RNS threads can leak between " - "sessions. Update peer_sideband.stop().", + "[peer_sideband] WARNING: SidebandCore.cleanup is gone — " + "Sideband may have renamed its teardown; RNS threads can leak " + "between sessions. Update peer_sideband.stop().", flush=True, ) self._core = None diff --git a/Tests/interop/run_peer.py b/Tests/interop/run_peer.py index 523db3d..4e61b89 100644 --- a/Tests/interop/run_peer.py +++ b/Tests/interop/run_peer.py @@ -77,8 +77,8 @@ def main() -> int: seen_taps += 1 # Print the full source LXMF delivery-destination hash + the # message hash so callers can match against the sender's - # `[PY] started identity=… destination=…` log line and the - # sender's `[PY] delivery state=delivered` line — that + # `[RNS] started identity=… destination=…` log line and the + # sender's `[RNS] delivery state=delivered` line — that # three-way match is the rigorous "same message, same source, # signature valid" check the harness relies on. src_hex = lxm.source_hash.hex() if lxm.source_hash else "?" diff --git a/Tests/interop/test_attachments.py b/Tests/interop/test_attachments.py index 1d10379..94a971d 100644 --- a/Tests/interop/test_attachments.py +++ b/Tests/interop/test_attachments.py @@ -275,12 +275,12 @@ def test_file_sideband_to_ios(sim, sideband): def _wait_for_diag_inbound(sim, *, content: str, timeout: float = 30.0) -> None: - """Block until `[PY] inbound` for this content lands in diag.log so + """Block until `[RNS] inbound` for this content lands in diag.log so the Chats list has the conversation row Maestro will tap on.""" deadline = time.time() + timeout while time.time() < deadline: for line in reversed(sim._tail_diag(800)): - if "[PY] inbound source=" in line and content in line: + if "[RNS] inbound source=" in line and content in line: return time.sleep(0.4) pytest.fail(f"iOS didn't record inbound for {content!r} within {timeout}s")