|
1 | 1 | # Columba-iOS interop test harness |
2 | 2 |
|
3 | | -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. |
| 3 | +Hermetic end-to-end interop suite for Columba-iOS, modelled on Columba-Android's |
| 4 | +`tests/interop/`. Pins iOS-side wire-format correctness against a reference |
| 5 | +Sideband peer running headless on the same host. The pytest suite |
| 6 | +(`test_attachments.py`, `test_telemetry.py`) drives the iOS Columba simulator |
| 7 | +via Maestro / `lxma://` deep links and asserts delivery on the Sideband side |
| 8 | +(and the reverse). 14 cases: text / image (opportunistic, direct, propagated) / |
| 9 | +file, both directions, plus location telemetry, custom-meta, periodic-share |
| 10 | +toggle, and cease. |
4 | 11 |
|
5 | 12 | ## Topology |
6 | 13 |
|
7 | 14 | ``` |
8 | | -┌─────────────────────────────────┐ ┌──────────────────────────────┐ |
9 | | -│ iOS Simulator (iPhone 17 …) │ │ Sideband-headless peer │ |
10 | | -│ Columba.app │ │ sbapp.sideband.core (in- │ |
11 | | -│ identity: 7030e48e… │ │ process via peer_sideband) │ |
12 | | -│ TCPClient → 127.0.0.1:4242 │ │ identity: 95617c21… │ |
13 | | -│ ↑ │ │ ↑ (RNS shared instance) │ |
14 | | -└───┼──────────────────────────────┘ └───┼──────────────────────────┘ |
| 15 | +┌──────────────────────────────────┐ ┌──────────────────────────────┐ |
| 16 | +│ iOS Simulator (iPhone 17) │ │ Sideband-headless peer │ |
| 17 | +│ Columba.app │ │ sbapp.sideband.core (in- │ |
| 18 | +│ TCPClient → 127.0.0.1:4242 │ │ process via peer_sideband) │ |
| 19 | +│ (auto-seeded by conftest) │ │ ↑ shared instance :37428 │ |
| 20 | +└───┼──────────────────────────────┘ └───┼──────────────────────────┘ |
15 | 21 | │ │ |
16 | | - └─────────────┐ ┌──────────────────┘ |
17 | | - ▼ ▼ |
18 | | - ┌───────────────────┐ |
19 | | - │ rnsd (Mac) │ |
20 | | - │ TCPServer :4242 │ |
21 | | - │ share_instance Y │ |
22 | | - └───────────────────┘ |
| 22 | + │ TCP :4242 LocalServer │ :37428 |
| 23 | + └──────────────┐ ┌─────────────────┘ |
| 24 | + ▼ ▼ |
| 25 | + ┌─────────────────────────────┐ |
| 26 | + │ lxmd (network.lxmf.lxmd) │ ← owns the shared RNS instance |
| 27 | + │ "Columba Hub" prop node │ enable_transport = True |
| 28 | + │ TCPServer :4242 │ share_instance = Yes |
| 29 | + │ LocalServer :37428 │ |
| 30 | + └─────────────────────────────┘ |
| 31 | + ▲ ▲ |
| 32 | + │ (also clients of the shared instance) |
| 33 | + rnsd nomadnet |
23 | 34 | ``` |
24 | 35 |
|
25 | | -All three RNS endpoints share the host transport (rnsd bridges the simulator's TCPClient to Sideband's shared-instance socket). No physical phone required. |
| 36 | +**`lxmd` — not rnsd — owns the shared RNS instance + the `:4242` TCPServer.** |
| 37 | +rnsd and nomadnet run alongside it as shared-instance *clients*. lxmd is the |
| 38 | +transport node (`enable_transport = True`) that bridges the sim's TCPClient to |
| 39 | +Sideband's shared-instance connection, so an announce/message from one side |
| 40 | +reaches the other. No physical phone required. |
26 | 41 |
|
27 | 42 | ## Components |
28 | 43 |
|
29 | | -- **`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`). |
30 | | -- **`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. |
31 | | -- **`flows/`** — Maestro YAML flows for driving the iOS Columba simulator UI. |
32 | | -- **`screenshots/`** — Where Maestro test runs write `takeScreenshot:` outputs. Gitignored. |
| 44 | +- **`conftest.py`** — pytest fixtures. Session-scoped, self-contained: |
| 45 | + - `sideband` — boots the in-process `SidebandPeer` once per session. |
| 46 | + - `_ensure_sim_interface` — **auto-seeds** a TCP-client interface |
| 47 | + (`127.0.0.1:4242`) into Columba's interface store and relaunches, so a |
| 48 | + fresh sim install (which comes up with 0 interfaces) bridges to lxmd with |
| 49 | + no manual "Settings → Add Interface" step. Override host/port with |
| 50 | + `RNSD_TCP_HOST` / `RNSD_TCP_PORT`. |
| 51 | + - `_bootstrap_paths` — cold-starts the bidirectional path table (each side |
| 52 | + must have heard the other's announce), then pins lxmd's propagation node |
| 53 | + on the sim (discovered via `lxmd --status`) so the propagated test runs |
| 54 | + deterministically instead of skipping on lxmd's 5-min announce cadence. |
| 55 | + - `Simulator` helper — reads the app's `Documents/diag.log` (the |
| 56 | + `[RNS] started/inbound/announce/delivery` lines — note the backend-agnostic |
| 57 | + `[RNS]` prefix, renamed from `[PY]` in the dual-backend refactor), fires |
| 58 | + `lxma://` deep links via Maestro, and resolves the sim's identity / LXMF |
| 59 | + delivery hash. |
| 60 | +- **`peer_sideband.py`** — `SidebandPeer`, the in-process Sideband daemon |
| 61 | + wrapper. Provides `start()`, `identity_hex`, `send_text()`, |
| 62 | + `wait_for_message()`, and `wait_for_tapped_message()` that sees every inbound |
| 63 | + `LXMessage` *before* Sideband filters it — the diagnostic surface for "iOS |
| 64 | + shows delivered but message disappears" bugs (fires at the RNS layer before |
| 65 | + any LXMF processing). Teardown uses `SidebandCore.cleanup()`. |
| 66 | +- **`run_peer.py`** — Standalone runner (`python run_peer.py`) to boot the peer |
| 67 | + interactively and print every inbound `LXMessage`. Used while debugging; the |
| 68 | + pytest suite drives `SidebandPeer` directly. |
| 69 | +- **`flows/`** — Maestro YAML flows that drive the Columba simulator UI. |
| 70 | +- **`screenshots/`** — Maestro `takeScreenshot:` outputs (gitignored). |
33 | 71 |
|
34 | 72 | ## Prerequisites |
35 | 73 |
|
36 | | -- macOS with Xcode 26+ and an iOS 26 simulator booted |
37 | | -- `rnsd` running with a TCPServer on `127.0.0.1:4242` and `share_instance = Yes` |
38 | | -- Sideband checkout at `~/repos/Sideband` |
39 | | -- Python venv with the RNS / LXMF wheels installed (the same venv `rnsd` uses is fine — `~/.reticulum-host/venv`) |
40 | | -- `maestro` installed (`brew install maestro`) |
41 | | -- Columba.app built for the simulator (see "Build" below) |
42 | | - |
43 | | -## Build |
| 74 | +- macOS with Xcode 26+ and **one** iOS simulator booted (iPhone 17). |
| 75 | +- **`lxmd` running** as the shared instance: `enable_transport = True`, |
| 76 | + `share_instance = Yes`, and a `[[TCP Server]]` on `127.0.0.1:4242` |
| 77 | + (config in `~/.lxmd/config` + `~/.reticulum/config`). rnsd/nomadnet may run |
| 78 | + alongside as clients but lxmd is what the sim and Sideband bridge through. |
| 79 | +- Sideband checkout at `~/repos/Sideband` (override with `SIDEBAND_SRC`). |
| 80 | +- A Python env with `RNS` / `LXMF` / `pytest` (the host venv |
| 81 | + `~/.reticulum-host/venv` works). |
| 82 | +- `maestro` installed (`brew install maestro`); JDK 21 on `JAVA_HOME` for it. |
| 83 | +- Columba.app **installed** on the booted sim (built `Debug` so the `lxma://` |
| 84 | + test deep links — which are `#if DEBUG`-gated in production — are present). |
| 85 | + The TCP interface to lxmd is seeded automatically by the suite; you do **not** |
| 86 | + need to configure it by hand. |
| 87 | + |
| 88 | +## Build + install the app |
44 | 89 |
|
45 | 90 | ```bash |
46 | 91 | cd ~/repos/Columba-iOS |
47 | 92 | xcrun simctl boot "iPhone 17" |
48 | | -xcodebuild -project Columba.xcodeproj -scheme Columba -configuration Debug \ |
| 93 | +xcodebuild -scheme Columba -configuration Debug \ |
49 | 94 | -destination "platform=iOS Simulator,name=iPhone 17" \ |
50 | 95 | -derivedDataPath /tmp/columba-sim-build build |
51 | | - |
52 | | -xcrun simctl install booted /tmp/columba-sim-build/Build/Products/Debug-iphonesimulator/ColumbaApp.app |
| 96 | +xcrun simctl install booted \ |
| 97 | + /tmp/columba-sim-build/Build/Products/Debug-iphonesimulator/ColumbaApp.app |
53 | 98 | ``` |
54 | 99 |
|
55 | | -## Run a manual diagnostic |
56 | | - |
57 | | -In one shell, boot the Sideband peer: |
| 100 | +## Run the suite |
58 | 101 |
|
59 | 102 | ```bash |
60 | | -~/.reticulum-host/venv/bin/python tests/interop/run_peer.py --watch |
61 | | -# prints identity_hex; leave running |
| 103 | +cd ~/repos/Columba-iOS/Tests/interop |
| 104 | +export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home |
| 105 | +python3 -m pytest -v |
62 | 106 | ``` |
63 | 107 |
|
64 | | -In another shell, launch Columba on the simulator with the TCP-hub env so it |
65 | | -joins the same rnsd: |
| 108 | +The session fixtures handle everything else: seed the sim's lxmd interface, |
| 109 | +boot the Sideband peer, bootstrap paths, pin the propagation node. Expect |
| 110 | +**14 passed**. (A run takes ~5 min — Sideband boot + a Maestro flow / deep-link |
| 111 | +send + delivery wait per case.) |
| 112 | + |
| 113 | +## Manual diagnostic |
66 | 114 |
|
67 | 115 | ```bash |
68 | | -SIMCTL_CHILD_COLUMBA_TCP_HUB=127.0.0.1:4242 \ |
69 | | - xcrun simctl launch booted network.columba.Columba |
| 116 | +SIDEBAND_SRC=~/repos/Sideband python3 run_peer.py # prints identity_hex, watches the tap |
70 | 117 | ``` |
71 | | - |
72 | | -Drive a send via Maestro: |
73 | | - |
| 118 | +Then drive a flow against the booted sim: |
74 | 119 | ```bash |
75 | 120 | maestro --device "$(xcrun simctl list devices booted | awk '/Booted/{print $NF}' | tr -d '()')" \ |
76 | | - test tests/interop/flows/send_text_to_sideband.yaml |
| 121 | + test flows/send_text_to_peer.yaml |
77 | 122 | ``` |
78 | | - |
79 | | -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. |
80 | | - |
81 | | -## Future: pytest-driven runs |
82 | | - |
83 | | -Like Columba-Android's suite, individual interop tests will be pytest cases that: |
84 | | -1. Spawn `SidebandPeer`, |
85 | | -2. Drive Maestro to send from iOS, |
86 | | -3. Assert on the tap (and/or the Sideband DB), |
87 | | -4. Reverse direction: `peer.send_text(ios_hash, …)` + Maestro screenshot/assertion that the bubble appeared on iOS. |
88 | | - |
89 | | -The `screenshots/` directory captures the per-step UI state for triage when an assertion fails. |
| 123 | +The tap prints every inbound `LXMessage` with its `signature_validated` flag and |
| 124 | +field-key set. If Columba reports "delivered" but no tap entry appears, the bug |
| 125 | +is on the iOS send wire; if the tap shows `signature_validated=False`, iOS's |
| 126 | +announce isn't reaching Sideband; if the content is right but Sideband's DB |
| 127 | +isn't, the issue is downstream of the upstream LXMF callback. |
| 128 | + |
| 129 | +## Troubleshooting |
| 130 | + |
| 131 | +- **Every case errors at setup with "sim never heard the peer announce"** (and |
| 132 | + more broadly, devices connect but receive nothing — `rx=0` on their links): |
| 133 | + **lxmd is wedged.** It accepts TCP connections but stops routing packets |
| 134 | + (symptom: `~/.reticulum/logfile` stops growing, lxmd at 0% CPU). This also |
| 135 | + takes down real phones bridging through it. Fix — bounce it: |
| 136 | + ```bash |
| 137 | + launchctl kickstart -k gui/$(id -u)/network.lxmf.lxmd |
| 138 | + ``` |
| 139 | + Confirm it recovered: `lxmd --status` shows peers/traffic and the logfile |
| 140 | + resumes `Valid announce for …` lines. |
| 141 | +- **`test_image_..._propagated` skips:** the suite pins lxmd's prop node via |
| 142 | + `lxmd --status`; a skip means lxmd was unreachable at bootstrap. Check lxmd |
| 143 | + is up and `--status` works. |
| 144 | +- **Suite can't read the sim identity / "Couldn't find `[RNS] started…`":** |
| 145 | + the app must be a `Debug` build (so the deep-link handlers exist) and must |
| 146 | + have launched at least once (it clears + writes `diag.log` on launch). |
0 commit comments