test(duty-cycle): run the suite against an older openhop_core too

The tests that cover per-channel metering ask a fabric which radio will carry a
packet, which an openhop_core from before the selector change cannot answer. Run
against one, fourteen of them failed -- on a repeater whose whole point is that
the core change is optional.

Tests that genuinely need the newer core (they drive build_radio_stack through a
real FabricRadio) are skipped when it is absent. The engine tests instead use a
fabric double that implements core's resolve rule and nothing else: explicit
radio_id wins, then the selector, then default_radio. That keeps the engine
measured against the contract rather than against a convenience, and the real
RFFabric is still what runs wherever the installed core supports it.

One fan-out test asserted the engine names the egress radio. What must not
change is which radio transmits; whether the engine names it or leaves the
choice to the fabric depends on the core underneath, so it accepts either.

Against openhop_core c95a684: 2154 pass, 6 skipped. Against dev with the
selector change: 2157 pass, 3 skipped.
This commit is contained in:
agessaman committed 2026-09-13 13:38:59 -07:00
1 parent d6f1a4c73b
commit 42e7939447
3 files changed
+74 -12

No files matched your search

+5 -3
View File
@@ -550,9 +550,11 @@ async def test_local_origin_default_mode_keeps_single_fabric_send():
rig = _Rig({"repeat_on_ingress": True}) # origin_tx defaults to "default"
assert await rig.handler(_flood(), {}, local_transmission=True) is True
assert rig.air.order == ["local"]
# Same radio on the air as when the fabric chose at send time; the engine now
# names it instead, so the duty cycle can be charged to the right channel.
assert rig.sent_radio_ids == ["local"]
# Same radio on the air either way. With a core that passes the ingress radio
# to the TX policy the engine names it, so the duty cycle can be charged to
# the right channel; without one it leaves the choice to the fabric, as it
# always did. What must not change is which radio transmits.
assert rig.sent_radio_ids in (["local"], [None])
assert rig.records()[-1]["tx_radio_ids"] == ["local"]
+15
View File
@@ -15,6 +15,18 @@ from repeater.config import (
)
def _core_selects_by_ingress() -> bool:
from openhop_core.rf_fabric import RFFabric
return fabric_selects_by_ingress_radio(RFFabric())
needs_ingress_core = pytest.mark.skipif(
not _core_selects_by_ingress(),
reason="installed openhop_core does not pass a packet's ingress radio to the TX selector",
)
class _FakeRadio:
def __init__(self, name="r"):
self.name = name
@@ -126,6 +138,7 @@ def test_build_radio_stack_multi_wraps_fabric():
assert radio.fabric._tx_selector is not None
@needs_ingress_core
@pytest.mark.asyncio
async def test_sticky_tx_uses_the_packets_own_ingress_radio():
a = _FakeRadio("a")
@@ -194,6 +207,7 @@ def test_tx_mode_all_rejected():
build_radio_stack(cfg)
@needs_ingress_core
@pytest.mark.asyncio
async def test_bridge_tx_crosses_to_other_radio():
"""RX on local -> TX on link; RX on link -> TX on local."""
@@ -423,6 +437,7 @@ def test_an_older_core_keeps_the_behaviour_it_always_had():
assert fabric.selector(b"frame") == "local"
@needs_ingress_core
def test_a_current_core_is_given_the_selector_that_routes_by_ingress_radio():
from openhop_core.rf_fabric import RFFabric
+54 -9
View File
@@ -24,7 +24,7 @@ from openhop_core.protocol.constants import (
from openhop_core.rf_fabric import RFFabric
from repeater.airtime import AirtimeBudgets, AirtimeManager
from repeater.config import _apply_fabric_tx_mode
from repeater.config import _apply_fabric_tx_mode, fabric_selects_by_ingress_radio
from repeater.engine import RepeaterHandler
WIDE = {
@@ -189,16 +189,61 @@ def test_a_retune_keeps_what_is_already_on_the_air():
# ---------------------------------------------------------------------------
def _fabric(radio_ids, default_radio_id=None, tx_mode="default"):
"""A real RFFabric with the real tx_mode selectors bound to it.
CORE_SELECTS_BY_INGRESS = fabric_selects_by_ingress_radio(RFFabric())
Not a stub. The one review finding that survived two passes came from an
engine that re-stated the fabric's dispatch rules and then diverged from
them; a fixture that also re-states them cannot catch that. This registers
real radios and runs `_apply_fabric_tx_mode`, so the answer the engine gets
is the answer the node would get.
class _ContextFabric:
"""The fabric contract these tests need, for a core that does not have it yet.
Used only when the installed openhop_core predates passing the ingress radio
to a TX selector; with a current core every test below runs against the real
RFFabric. What it implements is core's rule and nothing more -- an explicit
radio_id wins, then the selector, then default_radio -- so the engine is
still measured against the contract rather than against a convenience.
"""
fabric = RFFabric()
def __init__(self):
self.radios = OrderedDict()
self._default_radio_id = None
self._selector = None
def register_radio(self, radio, *, radio_id):
self.radios[radio_id] = radio
if self._default_radio_id is None:
self._default_radio_id = radio_id
def set_default_radio(self, radio_id):
self._default_radio_id = radio_id
@property
def default_radio_id(self):
return self._default_radio_id
def get_radio(self, radio_id):
return self.radios[radio_id]
def set_tx_selector(self, selector):
self._selector = selector
def resolve_tx_radio_id(self, data, radio_id=None, *, rx_radio_id=None):
if radio_id is not None:
return radio_id
if self._selector is not None:
selected = self._selector(data, rx_radio_id)
if selected is not None:
return selected
return self._default_radio_id
def _fabric(radio_ids, default_radio_id=None, tx_mode="default"):
"""A fabric with the real tx_mode selectors bound to it.
The real RFFabric where the installed core supports the ingress radio. Not a
stub that restates the dispatch rules either way: the one review finding
that survived two passes came from an engine that re-stated them and then
diverged, and a fixture that also restates them cannot catch that.
"""
fabric = RFFabric() if CORE_SELECTS_BY_INGRESS else _ContextFabric()
for radio_id in radio_ids:
fabric.register_radio(object(), radio_id=radio_id)
if default_radio_id in radio_ids: