Files
potato-mesh/tests/test_reticulum_scope_unit.py

713 lines
27 KiB
Python

# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for the Reticulum interface scope (SPEC RN4 as amended, RE3, RE4).
Unset, ``RETICULUM_INTERFACES`` ingests announces that arrived on an RNode only;
``*`` ingests every interface; any other value is a substring allowlist.
The real :class:`ReticulumProvider` and its real announce handlers run against
a fake shared-instance RNS stack. Identities and destination hashes come from
the real RNS library, and every interface is a real RNS interface class created
without ``__init__`` (no serial port, no socket), so each one's ``str()`` is
exactly what ``rnsd`` returns over RPC and lists in ``get_interface_stats()``.
"""
from __future__ import annotations
import multiprocessing
import re
import sys
import types
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
import RNS as REAL_RNS # noqa: E402 - path setup
from RNS.Interfaces.AutoInterface import ( # noqa: E402 - path setup
AutoInterface,
AutoInterfacePeer,
)
from RNS.Interfaces.BackboneInterface import ( # noqa: E402 - path setup
BackboneClientInterface,
)
from RNS.Interfaces.LocalInterface import LocalClientInterface # noqa: E402
from RNS.Interfaces.RNodeInterface import RNodeInterface # noqa: E402
from RNS.Interfaces.RNodeMultiInterface import ( # noqa: E402 - path setup
RNodeMultiInterface,
RNodeSubInterface,
)
from RNS.Interfaces.TCPInterface import TCPClientInterface # noqa: E402
import data.mesh_ingestor.config as config # noqa: E402 - path setup
import data.mesh_ingestor.protocols.reticulum as mod # noqa: E402 - path setup
def _bare(cls, **attrs):
"""Return a real RNS interface built without ``__init__``: no hardware, no I/O.
Parameters:
cls: RNS interface class.
attrs: Attributes its ``__str__`` reads.
Returns:
The interface instance.
"""
obj = object.__new__(cls)
for key, value in attrs.items():
setattr(obj, key, value)
return obj
_MULTI = _bare(RNodeMultiInterface, name="Multi Radio")
_AUTO = _bare(AutoInterface, name="Default Interface")
IFACES = {
"rnode": _bare(RNodeInterface, name="RNode LoRa Interface"),
"tcp": _bare(
TCPClientInterface,
name="RNS Testnet Amsterdam",
target_ip="amsterdam.connect.reticulum.network",
target_port=4965,
),
"backbone": _bare(
BackboneClientInterface,
name="Backbone Peer",
target_ip="203.0.113.7",
target_port=4242,
),
"multi": _MULTI,
"rnode_sub": _bare(RNodeSubInterface, name="868 Low", parent_interface=_MULTI),
"auto": _AUTO,
"auto_peer": _bare(
AutoInterfacePeer, ifname="eth0", addr="fe80::1", parent_interface=_AUTO
),
"local": _bare(LocalClientInterface, socket_path="\0rns/default"),
}
"""Real RNS interfaces, keyed by role. ``local`` is this client's own socket."""
PEERS = (
("tcp_peer", "tcp", 3, "Amsterdam Hub User"),
("backbone_peer", "backbone", 2, "Backbone Peer User"),
("rnode_peer", "rnode", 1, "LoRa Neighbour"),
("rnode_sub_peer", "rnode_sub", 1, "Multi Radio Neighbour"),
("auto_peer", "auto_peer", 1, "LAN Peer"),
("own", "local", 0, "Operator Node"),
)
"""(label, arrival interface, hops, display name) per peer; ``own`` is this host."""
ALL = {label for label, *_ in PEERS}
"""Every peer label."""
FULL_STACK = ("rnode", "tcp", "backbone", "multi", "rnode_sub", "auto", "auto_peer")
"""The interfaces rnsd runs on the default fake stack."""
NO_RNODE_STACK = ("tcp", "backbone", "auto", "auto_peer")
"""A stack with IP interfaces only."""
_TCP = str(IFACES["tcp"])
"""The TCP interface's printed name, as rnsd reports it."""
def _stats_entry(iface) -> dict:
"""Return the ``get_interface_stats()`` entry RNS builds for *iface*.
Only the keys the scope can read: ``name`` is ``str(interface)``, ``type``
the class name, and a spawned interface also names its parent
(``Reticulum.get_interface_stats``).
Parameters:
iface: A real RNS interface.
Returns:
The stats entry.
"""
entry = {
"name": str(iface),
"short_name": str(getattr(iface, "name", None)),
"type": type(iface).__name__,
}
parent = getattr(iface, "parent_interface", None)
if parent is not None:
entry["parent_interface_name"] = str(parent)
return entry
class _Stack:
"""rnsd's state behind the fake RNS namespace.
Holds the interfaces rnsd runs, the path to each peer's ``lxmf.delivery``
destination, and who owns it. The operator also runs a nomadnet node, so
host discovery finds two local destinations of one identity (SPEC RE8).
"""
def __init__(self, interfaces=FULL_STACK, *, stats="ok"):
"""Build the stack.
Parameters:
interfaces: Keys of :data:`IFACES` rnsd runs.
stats: ``"ok"``, ``"missing"`` (the instance has no
``get_interface_stats``), or an exception every RPC raises.
"""
self.interfaces = [IFACES[key] for key in interfaces]
self.stats = stats
self.stats_calls = 0
self.registered: list = []
self.paths: dict[bytes, tuple[int, object]] = {}
self.owners: dict[bytes, object] = {}
self.dests: dict[str, bytes] = {}
self.transport_identity = REAL_RNS.Identity()
self.identities = {label: REAL_RNS.Identity() for label, *_ in PEERS}
for label, key, hops, _name in PEERS:
dest = REAL_RNS.Destination.hash(self.identities[label], "lxmf", "delivery")
self.dests[label] = dest
self.paths[dest] = (hops, IFACES[key])
self.owners[dest] = self.identities[label]
own = self.identities["own"]
nomad = REAL_RNS.Destination.hash(own, "nomadnetwork", "node")
self.paths[nomad] = (0, IFACES["local"])
self.owners[nomad] = own
def _rpc(self):
"""Raise the configured RPC failure, if any."""
if isinstance(self.stats, BaseException):
raise self.stats
def next_hop_if_name(self, dest) -> str:
"""Answer ``get_next_hop_if_name``: ``str()`` of rnsd's next-hop interface."""
self._rpc()
entry = self.paths.get(bytes(dest))
return str(entry[1]) if entry else "None"
def path_table(self, max_hops=None) -> list[dict]:
"""Answer ``get_path_table`` for paths within *max_hops*."""
self._rpc()
return [
{"hash": dest, "hops": hops, "interface": str(iface)}
for dest, (hops, iface) in self.paths.items()
if max_hops is None or hops <= max_hops
]
def interface_stats(self) -> dict:
"""Answer ``get_interface_stats``, counting the calls."""
self.stats_calls += 1
self._rpc()
return {
"interfaces": [_stats_entry(iface) for iface in self.interfaces],
"transport_id": None,
}
def _fake_rns(stack: _Stack):
"""Return a fake ``RNS`` namespace: a client attached to a shared ``rnsd``.
Parameters:
stack: The rnsd state the shared instance answers from.
Returns:
The namespace the provider reads as ``RNS``.
"""
shared = types.SimpleNamespace(
is_connected_to_shared_instance=True,
get_next_hop_if_name=stack.next_hop_if_name,
get_path_table=stack.path_table,
)
if stack.stats != "missing":
shared.get_interface_stats = stack.interface_stats
transport = types.SimpleNamespace(
PATHFINDER_M=128,
register_announce_handler=stack.registered.append,
deregister_announce_handler=lambda _handler: None,
hops_to=lambda dest: stack.paths.get(bytes(dest), (128, None))[0],
# This client's own path table: everything reads as the local socket.
next_hop_interface=lambda _dest: IFACES["local"],
internal_identity=lambda: stack.transport_identity,
)
return types.SimpleNamespace(
Reticulum=types.SimpleNamespace(
get_instance=lambda: shared, transport_enabled=lambda: False
),
Transport=transport,
Identity=types.SimpleNamespace(
recall=lambda dest, **_k: stack.owners.get(bytes(dest)),
recall_app_data=lambda _dest: None,
),
Destination=types.SimpleNamespace(hash=REAL_RNS.Destination.hash),
)
class _Run:
"""The real provider on a fake stack, with what it logged, counted and posted."""
def __init__(self, monkeypatch, tmp_path, scope, **stack_options):
"""Install the fake stack and the scope; nothing is connected yet.
Parameters:
monkeypatch: pytest fixture.
tmp_path: Directory used as ``RETICULUM_CONFIG_DIR`` (no config).
scope: Value for ``config.RETICULUM_INTERFACES``.
stack_options: Keyword arguments for :class:`_Stack`.
"""
self.stack = _Stack(**stack_options)
self.logs: list = []
self.upserts: list = []
self.seen = 0
self.iface = None
monkeypatch.setattr(mod, "RNS", _fake_rns(self.stack))
monkeypatch.setattr(config, "RETICULUM_INTERFACES", scope)
monkeypatch.setattr(config, "RETICULUM_CONFIG_DIR", str(tmp_path))
for name in ("RETICULUM_FREQ", "RETICULUM_PRESET", "INGESTOR_NODE_ID"):
monkeypatch.setattr(config, name, None)
monkeypatch.setattr(config, "CONNECTION", None)
monkeypatch.setattr(config, "LORA_FREQ", 867)
monkeypatch.setattr(config, "MODEM_PRESET", "SF8/BW125/CR5")
monkeypatch.setattr(
config, "_debug_log", lambda msg, **kw: self.logs.append((msg, kw))
)
monkeypatch.setattr(
mod.handlers,
"upsert_node",
lambda nid, node: self.upserts.append((nid, node)),
)
monkeypatch.setattr(mod.handlers, "_mark_packet_seen", self._count)
def _count(self) -> None:
"""Stand in for ``handlers._mark_packet_seen``."""
self.seen += 1
def connect(self) -> "_Run":
"""Connect the real provider; returns ``self`` for chaining."""
self.iface, _target, _next = mod.ReticulumProvider().connect(
active_candidate=None
)
return self
def announce(self, *labels: str) -> None:
"""Dispatch each peer's ``lxmf.delivery`` announce (default: every peer)."""
handler = next(
h for h in self.stack.registered if h.aspect_filter == "lxmf.delivery"
)
names = {label: name for label, _key, _hops, name in PEERS}
for label in labels or [label for label, *_ in PEERS]:
handler.received_announce(
destination_hash=self.stack.dests[label],
announced_identity=self.stack.identities[label],
app_data=names[label].encode("utf-8"),
)
def node_id(self, label: str) -> str:
"""Return the canonical node id of the peer *label*."""
return mod._reticulum_node_id(self.stack.identities[label].hash)
@property
def ingested(self) -> set[str]:
"""Labels of the peers that were posted."""
label_of = {self.node_id(label): label for label in ALL}
return {label_of[node_id] for node_id, _node in self.upserts}
def record(self, label: str) -> dict:
"""Return the node record last posted for *label*."""
return [node for nid, node in self.upserts if nid == self.node_id(label)][-1]
def warnings(self) -> list[str]:
"""Return every message logged at ``warn``."""
return [msg for msg, kw in self.logs if kw.get("severity") == "warn"]
def startup(self) -> dict:
"""Return the fields of the first "listener registered" line."""
return next(kw for msg, kw in self.logs if "listener registered" in msg)
# ---------------------------------------------------------------------------
# The RNode default (SPEC RN4 as amended)
# ---------------------------------------------------------------------------
def test_unset_scope_drops_a_tcp_announce(monkeypatch, tmp_path):
"""Unset, a TCP announce from 3 hops out is neither posted nor counted."""
run = _Run(monkeypatch, tmp_path, ()).connect()
assert mod._announce_admitted(3, _TCP) is False
run.announce("tcp_peer")
assert run.upserts == []
assert run.seen == 0
def test_unset_scope_drops_backbone_and_autointerface_announces(monkeypatch, tmp_path):
"""A Backbone peer and an AutoInterface peer are IP, not RNode: dropped."""
run = _Run(monkeypatch, tmp_path, ()).connect()
run.announce("backbone_peer", "auto_peer")
assert run.ingested == set()
assert run.seen == 0
def test_unset_scope_admits_an_rnode_announce(monkeypatch, tmp_path):
"""An announce heard on an RNodeInterface is ingested and counted."""
run = _Run(monkeypatch, tmp_path, ()).connect()
run.announce("rnode_peer")
assert run.ingested == {"rnode_peer"}
assert run.seen == 1
assert run.record("rnode_peer")["interface"] == str(IFACES["rnode"])
def test_unset_scope_admits_an_rnode_multi_subinterface(monkeypatch, tmp_path):
"""A multi-radio RNode's sub-interface is an RNode, though its name says not.
It prints as ``<parent>[<sub>]`` with no type text, so the class that
``get_interface_stats()`` reports for that name decides.
"""
run = _Run(monkeypatch, tmp_path, ()).connect()
run.announce("rnode_sub_peer")
assert run.ingested == {"rnode_sub_peer"}
assert run.record("rnode_sub_peer")["interface"] == "Multi Radio[868 Low]"
def test_unset_scope_keeps_this_machines_announces(monkeypatch, tmp_path):
"""0-hop announces and the host's own destinations stay in (SPEC RE4)."""
run = _Run(monkeypatch, tmp_path, ()).connect()
run.announce("own")
assert run.ingested == {"own"}
host = run.node_id("own")
assert run.startup()["node_id"] == host
snapshot = mod.ReticulumProvider().node_snapshot_items(run.iface)
assert host in {node_id for node_id, _node in snapshot}
@pytest.mark.parametrize("raw", ["", '""', "''", " "])
def test_compose_empty_value_is_the_rnode_scope(monkeypatch, tmp_path, raw):
"""What Compose, the image and a null NixOS option deliver is the default.
``${RETICULUM_INTERFACES:-}`` and both ``data/Dockerfile`` stages deliver an
empty string; a quoted default would deliver ``""``.
"""
run = _Run(monkeypatch, tmp_path, config._parse_reticulum_interfaces(raw))
run.connect()
assert mod._announce_admitted(3, _TCP) is False
run.announce()
assert run.ingested == {"rnode_peer", "rnode_sub_peer", "own"}
def test_star_ingests_every_interface(monkeypatch, tmp_path):
"""``*`` is the old default: every interface, IP included."""
run = _Run(monkeypatch, tmp_path, config._parse_reticulum_interfaces("*"))
run.connect().announce()
assert run.ingested == ALL
assert run.seen == len(PEERS)
def test_explicit_list_keeps_the_substring_rule(monkeypatch, tmp_path):
"""A list still matches names, so ``rnode`` misses a sub-interface."""
run = _Run(monkeypatch, tmp_path, config._parse_reticulum_interfaces("RNode"))
run.connect().announce()
assert run.ingested == {"rnode_peer", "own"}
assert run.warnings() == []
def test_the_scope_rules_apply_in_order(monkeypatch, tmp_path):
"""0 hops, then ``*``, then a missing name, then the default or the list."""
_Run(monkeypatch, tmp_path, ())
assert mod._announce_admitted(0, None) is True
assert mod._announce_admitted(1, None) is False
assert mod._announce_admitted(1, "") is False
monkeypatch.setattr(config, "RETICULUM_INTERFACES", ("*",))
assert mod._announce_admitted(1, None) is True
assert mod._announce_admitted(None, _TCP) is True
# ---------------------------------------------------------------------------
# Connect-time warnings and the startup line
# ---------------------------------------------------------------------------
def test_connect_warns_once_when_the_stack_has_no_rnode(monkeypatch, tmp_path):
"""No RNode on the stack: one warning per connect, and only 0 hops survive."""
run = _Run(monkeypatch, tmp_path, (), interfaces=NO_RNODE_STACK).connect()
scope_warnings = [w for w in run.warnings() if "RETICULUM_INTERFACES=*" in w]
assert len(scope_warnings) == 1
assert "No RNode interface" in scope_warnings[0]
# Classes, never names: a spawned interface's name can carry a peer address.
[fields] = [kw for msg, kw in run.logs if "No RNode interface" in msg]
assert fields["interface_classes"] == [
"AutoInterface",
"AutoInterfacePeer",
"BackboneClientInterface",
"TCPClientInterface",
]
names = [str(IFACES[key]) for key in NO_RNODE_STACK]
assert not any(name in str(fields) for name in names)
run.announce("tcp_peer", "backbone_peer", "auto_peer", "own")
# Never "fall back to all": the IP peers stay out.
assert run.ingested == {"own"}
assert len(run.warnings()) == 1
run.connect()
assert len(run.warnings()) == 2
def test_connect_warns_when_the_stats_rpc_is_rejected(monkeypatch, tmp_path):
"""A rejected RPC turns every name into LocalInterface[...]: say so.
A shared instance authenticates RPC with a key from the config dir's
identity, so an ingestor on another config dir is refused (SPEC RE3).
"""
refused = multiprocessing.AuthenticationError("digest received was wrong")
run = _Run(monkeypatch, tmp_path, (), stats=refused).connect()
[(warning, fields)] = [
(msg, kw) for msg, kw in run.logs if kw.get("severity") == "warn"
]
assert "RETICULUM_CONFIG_DIR" in warning
assert fields["error_class"] == "AuthenticationError"
run.announce()
assert run.ingested == {"own"}
def test_stack_without_stats_falls_back_to_the_rnode_name(monkeypatch, tmp_path):
"""No stats to read: the name decides, silently (no warning at connect)."""
run = _Run(monkeypatch, tmp_path, (), stats="missing").connect()
assert run.warnings() == []
run.announce()
# A single-radio RNode prints "RNodeInterface[...]"; a sub-interface does not.
assert run.ingested == {"rnode_peer", "own"}
@pytest.mark.parametrize("raw", ["testnet", "*"])
def test_an_explicit_scope_never_warns_about_a_missing_rnode(
monkeypatch, tmp_path, raw
):
"""A list or ``*`` is the operator's choice; only the default checks."""
run = _Run(
monkeypatch,
tmp_path,
config._parse_reticulum_interfaces(raw),
interfaces=NO_RNODE_STACK,
)
assert run.connect().warnings() == []
@pytest.mark.parametrize(
("raw", "expected"),
[("", "rnode"), ("*", "*"), ("RNode, serial", ["rnode", "serial"])],
)
def test_startup_line_names_the_scope(monkeypatch, tmp_path, raw, expected):
"""The startup line says which scope is active, never "all"."""
run = _Run(monkeypatch, tmp_path, config._parse_reticulum_interfaces(raw))
assert run.connect().startup()["interfaces"] == expected
# ---------------------------------------------------------------------------
# The provider's radio-metadata tag follows the same class check (SPEC RL1)
# ---------------------------------------------------------------------------
def test_the_provider_tags_both_rnode_peers_with_radio_metadata(monkeypatch, tmp_path):
"""The provider tags both RNode peers' records with the LoRa values.
``run.record`` is the record the provider hands to ``handlers.upsert_node``,
which ``_Run`` stubs. The real one stamps the configured values on every
posted record whatever the interface, a known gap outside this provider.
"""
run = _Run(monkeypatch, tmp_path, ("*",)).connect()
run.announce()
stamped = {label for label in run.ingested if "lora_freq" in run.record(label)}
assert stamped == {"rnode_peer", "rnode_sub_peer"}
# ---------------------------------------------------------------------------
# The name-to-class map (reticulum_interfaces)
# ---------------------------------------------------------------------------
class _Instance:
"""A stack whose interface list can change, or fail, between reads."""
def __init__(self, *keys: str):
"""List the :data:`IFACES` named by *keys*."""
self.entries = [_stats_entry(IFACES[key]) for key in keys]
self.calls = 0
self.error: BaseException | None = None
def get_interface_stats(self) -> dict:
"""Return the stats, or raise :attr:`error`."""
self.calls += 1
if self.error is not None:
raise self.error
return {"interfaces": list(self.entries)}
@pytest.fixture
def classes():
"""Return :mod:`data.mesh_ingestor.protocols.reticulum_interfaces`.
Reached through the provider module that imports it, so this file still
imports, and its regression tests still run, on a tree without it.
"""
return mod.reticulum_interfaces
def _cache(classes, serving, now=(0.0,)):
"""Return an ``InterfaceClassCache`` reading ``serving[0]`` as the instance.
Parameters:
classes: The :func:`classes` fixture.
serving: One-item list holding the instance, so a test can swap it.
now: One-item sequence holding the clock reading.
"""
return classes.InterfaceClassCache(lambda: serving[0], clock=lambda: now[0])
def test_the_provider_reads_the_running_stack(monkeypatch, tmp_path):
"""The provider's map asks ``RNS.Reticulum.get_instance()`` (shared, RE3)."""
run = _Run(monkeypatch, tmp_path, ())
assert mod._running_instance() is mod.RNS.Reticulum.get_instance()
assert mod._INTERFACE_CLASSES.class_of(_TCP) == "TCPClientInterface"
assert run.stack.stats_calls == 1
def test_class_map_is_cached_and_reread_on_a_miss(classes):
"""A hit costs nothing; a miss re-reads, at most once per refresh window."""
now = [100.0]
stack = _Instance("rnode")
cache = _cache(classes, [stack], now)
rnode, sub = str(IFACES["rnode"]), str(IFACES["rnode_sub"])
assert cache.class_of(rnode) == "RNodeInterface"
assert cache.class_of(rnode) == "RNodeInterface"
assert stack.calls == 1
# rnsd spawns a sub-interface after the read: inside the window it is unknown.
stack.entries.append(_stats_entry(IFACES["rnode_sub"]))
now[0] += classes.REFRESH_SECONDS - 1
assert cache.class_of(sub) is None
assert stack.calls == 1
now[0] += 1
assert cache.class_of(sub) == "RNodeSubInterface"
assert stack.calls == 2
def test_read_refreshes_now_and_reports_the_failure(classes):
"""``read`` ignores the window; an RPC error reaches the caller."""
stack = _Instance("tcp")
cache = _cache(classes, [stack])
assert cache.read() == {_TCP: "TCPClientInterface"}
assert cache.read() == {_TCP: "TCPClientInterface"}
assert stack.calls == 2
stack.error = multiprocessing.AuthenticationError("digest received was wrong")
with pytest.raises(multiprocessing.AuthenticationError):
cache.read()
# The last good map still answers.
assert cache.class_of(_TCP) == "TCPClientInterface"
def test_a_malformed_reply_keeps_the_previous_map(classes):
"""A reply without an interface list adds nothing and loses nothing."""
stack = _Instance("tcp")
cache = _cache(classes, [stack])
assert cache.read() == {_TCP: "TCPClientInterface"}
stack.get_interface_stats = lambda: {"transport_id": None}
assert cache.read() is None
assert cache.class_of(_TCP) == "TCPClientInterface"
def test_class_map_lookup_survives_a_dead_or_silent_stack(classes):
"""No instance, a raising accessor, no stats method, or a failing RPC: None."""
serving: list = [None]
cache = _cache(classes, serving)
assert cache.class_of(_TCP) is None
assert cache.read() is None
def _gone():
"""Stand in for an accessor of a stack that has shut down."""
raise RuntimeError("stack is gone")
assert classes.InterfaceClassCache(_gone).class_of(_TCP) is None
serving[0] = types.SimpleNamespace()
assert cache.class_of(_TCP) is None
assert cache.read() is None
stack = _Instance("tcp")
stack.error = OSError("rpc down")
serving[0] = stack
assert cache.class_of(_TCP) is None
# The failed read starts the window too: no retry storm per announce.
assert cache.class_of(_TCP) is None
assert stack.calls == 1
def test_a_new_instance_discards_the_map(classes):
"""The map belongs to the stack it was read from."""
serving = [_Instance("rnode")]
cache = _cache(classes, serving)
assert cache.class_of(str(IFACES["rnode"])) == "RNodeInterface"
serving[0] = _Instance("tcp")
assert cache.class_of(str(IFACES["rnode"])) is None
assert cache.class_of(_TCP) == "TCPClientInterface"
def test_interface_classes_skips_malformed_stats(classes):
"""Anything but a list of name/type entries is skipped, never fatal."""
parse = classes.interface_classes
assert parse(None) is None
assert parse({"transport_id": None}) is None
assert parse({"interfaces": "nope"}) is None
entries = [None, {"name": 1, "type": "X"}, {"name": "A[b]"}, _stats_entry(_MULTI)]
assert parse({"interfaces": entries}) == {
"RNodeMultiInterface[Multi Radio]": "RNodeMultiInterface"
}
def test_every_rnode_class_counts_as_an_rnode(classes):
"""The three RNode classes RNS ships, and nothing else."""
assert classes.RNODE_INTERFACE_CLASSES == {
"RNodeInterface",
"RNodeMultiInterface",
"RNodeSubInterface",
}
rnodes = {type(IFACES[key]).__name__ for key in ("rnode", "multi", "rnode_sub")}
assert rnodes == classes.RNODE_INTERFACE_CLASSES
# ---------------------------------------------------------------------------
# Operator docs (ACCEPTANCE RE-A16)
# ---------------------------------------------------------------------------
_OLD_DEFAULT = re.compile(
r"empty ingests|ingests everything|from every interface|"
r"\(the default\) ingests|admits everything"
)
"""Phrases that stated the old "empty ingests every interface" default."""
@pytest.mark.parametrize(
"name",
[
"README.md",
"flake.nix",
"configure.sh",
"data/mesh_ingestor/CONTRACTS.md",
"data/mesh_ingestor/config.py",
"data/mesh_ingestor/protocols/reticulum.py",
],
)
def test_docs_state_the_rnode_default(name):
"""No surface still says an unset scope ingests every interface."""
text = (REPO_ROOT / name).read_text(encoding="utf-8")
assert _OLD_DEFAULT.findall(text) == [], f"{name} still states the old default"
def test_readme_names_the_star_scope():
"""The README tells the operator how to get every interface back."""
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
assert "RETICULUM_INTERFACES=*" in readme