# Meshtasticator e2e — radio-level validation for hermes-meshtastic This directory is the **radio-level counterpart** to the contract suite (`plugin/tests/contract`). The contract suite guarantees the adapter matches the Hermes gateway API; it cannot prove the adapter actually talks to a Meshtastic node. These e2e tests boot the [Meshtasticator](https://meshtastic.org/docs/software/meshtasticator/) interactive simulator — which runs the **real MeshtasticD device software** per node with a simulated LoRa PHY — and drive the adapter against a live simulated node. | Layer | Validated by | |---|---| | Adapter ↔ Hermes gateway API | `plugin/tests/contract` (no deployment) | | Adapter ↔ real MeshtasticD TCP/protobuf session | **this suite** | | Inbound text across the (simulated) LoRa mesh | **this suite** | | Outbound chunked sends arriving at another node | **this suite** | | True RF (silicon LoRa, real propagation, duty cycle) | physical radio only | ## Layout ``` scripts/meshtasticator_e2e/ run_e2e.sh orchestrator: boots N nodes, runs pytest, tears down tcp_forward.py localhost->daemon TCP forward (remote docker daemons) README.md this file plugin/tests/e2e/ test_meshtasticator_e2e.py the e2e assertions (skip-gated) .gitea/workflows/ meshtasticator-e2e.yaml on-demand CI job (docker-enabled runner) ``` ## Prerequisites * **uv** + Python 3.13. * A contract venv with the real gateway **and** the plugin (with deps) installed: ```bash scripts/contract-test.sh # creates plugin/.venv-contract (hermes-agent pinned) uv pip install --python plugin/.venv-contract/bin/python -e plugin ``` (`-e plugin` installs the meshtastic client library the tests also use.) * A clone of Meshtasticator: `git clone https://github.com/meshtastic/meshtasticator` * Runtime for the simulator — pick one: * **Docker** (simplest, works on macOS/Windows): the simulator pulls the `meshtastic/meshtasticd` image on first run (slow once). Node TCP API ports are published on your docker host's localhost, which is what the simulator itself expects. * **Linux native**: build MeshtasticD's `native` PlatformIO target and pass the build dir with `--program`. * Optional but recommended: an isolated simulator venv, because Meshtasticator pins old client libs (`meshtastic~=2.6.1` in its requirements.txt): ```bash uv venv --python 3.13 /tmp/sim-venv uv pip install --python /tmp/sim-venv/bin/python \ -r /requirements.txt docker export SIM_VENV=/tmp/sim-venv # else the script uses plain `python3` ``` ## Run ```bash # Docker mode (default) MESHTASTICATOR_DIR=/path/to/meshtasticator \ scripts/meshtasticator_e2e/run_e2e.sh # Linux native mode MESHTASTICATOR_DIR=/path/to/meshtasticator \ scripts/meshtasticator_e2e/run_e2e.sh \ --mode native --program /path/to/firmware/.pio/build/native # Tune the scenario ... --nodes 3 --channel-index 0 --host 127.0.0.1 ``` The orchestrator boots the simulator, waits until `N` node TCP API ports accept connections (Meshtasticator assigns node *n* → port `4404 + n`), then runs `pytest plugin/tests/e2e` with `MESHTASTICATOR_E2E=1`. Simulator output lands in `meshtasticator-sim.log` at the repo root. **Remote docker daemon** (`DOCKER_HOST=tcp://host:2375`): the simulator's own control connections assume nodes listen on localhost, so the script starts a `tcp_forward.py` per node port (localhost → daemon host) before booting the simulator. Nothing is forwarded when the daemon is local (unset/unix `DOCKER_HOST`). Without `MESHTASTICATOR_E2E=1` the e2e module skips, so ordinary unit/CI runs stay fast and gateway-free. ## CI `.gitea/workflows/meshtasticator-e2e.yaml` runs the whole flow on a docker-enabled runner (on demand via `workflow_dispatch` until validated, then flip on the commented `pull_request` trigger): it builds the Hermes contract env (pinned like `release.yaml`), installs the plugin with deps, clones Meshtasticator at a pinned commit, installs the simulator's own requirements in an isolated venv, and calls `run_e2e.sh --mode docker`. Simulator logs print on failure. Only one e2e run at a time (the sim hardcodes the `Meshtastic` container/volume name on the shared daemon). ## What the tests do 1. **Inbound**: peer node 1 broadcasts text; the adapter (connected to node 0) must receive it as a real gateway `MessageEvent` (`host/port/channel_index` come from `E2E_PORTS`/`E2E_HOST`/`E2E_CHANNEL_INDEX`). 2. **Outbound**: the adapter sends a >180-char message; it must be chunked and the final chunk (`(i/n) … word59`) must arrive at peer node 1. A `_ProbeAdapter` subclass records inbound events instead of dispatching to a Hermes agent handler (no agent runs in these tests; dispatch is already covered by the contract suite). ## Status / open items (v1 scaffold) This is an initial, structurally validated scaffold. A full green run still needs to happen on a Linux/Docker host with a MeshtasticD build, and these items are expected to need iteration there: * **Node adjacency**: the simulator's default random placement may put nodes out of range. For deterministic tests, seed close-together coordinates (the sim supports `--from-file` with an `out/nodeConfig.yaml`). * **Channel semantics**: tests default to channel index 0 (stock primary channel). Mirroring production (channel 1 / private channel) requires configuring matching channels on the simulated nodes. * **Reconnect flow**: killing/restarting a single simulated node needs per-node lifecycle control (native mode subprocesses; docker mode currently runs all nodes in one container), so the adapter's watchdog reconnect is **not** yet exercised here. See the watchdog unit coverage in the adapter for now. * **CI validation run**: `.gitea/workflows/meshtasticator-e2e.yaml` is drafted and on-demand only; it still needs a first green run on the docker-enabled runner (topology assumptions: docker reachable from the job, node ports on the job's localhost via the automatic tcp forwards). Trigger it via `workflow_dispatch`, watch the sim log step, and only then enable the `pull_request` trigger.