ria-toolkit-oss/tests/agent/test_tx_safety.py
ben 22b035dbee
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Has been cancelled
Test with tox / Test with tox (3.10) (pull_request) Has been cancelled
Test with tox / Test with tox (3.11) (pull_request) Has been cancelled
Test with tox / Test with tox (3.12) (pull_request) Has been cancelled
Build Project / Build Project (3.12) (pull_request) Has been cancelled
Build Project / Build Project (3.11) (pull_request) Has been cancelled
Build Project / Build Project (3.10) (pull_request) Has been cancelled
format fixes
2026-04-20 13:51:15 -04:00

172 lines
5.3 KiB
Python

"""Agent-side TX interlocks: gain cap, freq ranges, duplicate sessions, disabled."""
from __future__ import annotations
import asyncio
from ria_toolkit_oss.agent.config import AgentConfig
from ria_toolkit_oss.agent.streamer import Streamer
from ria_toolkit_oss.sdr.mock import MockSDR
class FakeWs:
def __init__(self):
self.json_sent = []
self.bytes_sent = []
async def send_json(self, p):
self.json_sent.append(p)
async def send_bytes(self, b):
self.bytes_sent.append(b)
def _last_tx_status(ws):
frames = [m for m in ws.json_sent if m.get("type") == "tx_status"]
return frames[-1] if frames else None
def _tx_start(app_id="a", **radio):
rc = {
"device": "mock",
"buffer_size": 16,
"tx_sample_rate": 1_000_000,
"tx_center_frequency": 2.45e9,
"tx_gain": -20,
"underrun_policy": "zero",
}
rc.update(radio)
return {"type": "tx_start", "app_id": app_id, "radio_config": rc}
def _make_streamer(cfg):
built: list = []
def factory(device, identifier):
sdr = MockSDR(buffer_size=16)
built.append(sdr)
return sdr
ws = FakeWs()
s = Streamer(ws=ws, sdr_factory=factory, cfg=cfg)
return s, ws, built
def test_rejects_when_tx_disabled():
async def scenario():
s, ws, built = _make_streamer(AgentConfig(tx_enabled=False))
await s.on_message(_tx_start(tx_gain=-20, tx_center_frequency=2.45e9))
return s, ws, built
s, ws, built = asyncio.run(scenario())
status = _last_tx_status(ws)
assert status and status["state"] == "error"
assert "disabled" in status["message"].lower()
assert not built, "SDR should never have been constructed"
assert s._tx is None
def test_rejects_when_tx_gain_exceeds_cap():
async def scenario():
s, ws, built = _make_streamer(AgentConfig(tx_enabled=True, tx_max_gain_db=-15.0))
await s.on_message(_tx_start(tx_gain=-5, tx_center_frequency=2.45e9))
return ws, built
ws, built = asyncio.run(scenario())
status = _last_tx_status(ws)
assert status and status["state"] == "error"
assert "exceeds cap" in status["message"]
assert not built
def test_allows_gain_at_cap_boundary():
async def scenario():
s, ws, _ = _make_streamer(AgentConfig(tx_enabled=True, tx_max_gain_db=-10.0))
await s.on_message(_tx_start(tx_gain=-10, tx_center_frequency=2.45e9))
# Stop promptly to avoid keeping an executor thread around.
await asyncio.sleep(0.02)
await s.on_message({"type": "tx_stop", "app_id": "a"})
return ws
ws = asyncio.run(scenario())
states = [m["state"] for m in ws.json_sent if m.get("type") == "tx_status"]
assert "armed" in states
assert states[-1] == "done"
def test_rejects_when_freq_outside_ranges():
async def scenario():
s, ws, built = _make_streamer(
AgentConfig(
tx_enabled=True,
tx_allowed_freq_ranges=[[2.4e9, 2.5e9]],
)
)
await s.on_message(_tx_start(tx_center_frequency=5.8e9, tx_gain=-20))
return ws, built
ws, built = asyncio.run(scenario())
status = _last_tx_status(ws)
assert status and status["state"] == "error"
assert "outside allowed ranges" in status["message"]
assert not built
def test_allows_freq_inside_a_range():
async def scenario():
s, ws, _ = _make_streamer(
AgentConfig(
tx_enabled=True,
tx_allowed_freq_ranges=[[2.4e9, 2.5e9], [5.7e9, 5.8e9]],
)
)
await s.on_message(_tx_start(tx_center_frequency=5.75e9, tx_gain=-20))
await asyncio.sleep(0.02)
await s.on_message({"type": "tx_stop", "app_id": "a"})
return ws
ws = asyncio.run(scenario())
states = [m["state"] for m in ws.json_sent if m.get("type") == "tx_status"]
assert "armed" in states
assert states[-1] == "done"
def test_rejects_duplicate_tx_session():
async def scenario():
s, ws, _ = _make_streamer(AgentConfig(tx_enabled=True))
await s.on_message(_tx_start(app_id="a", tx_gain=-20, tx_center_frequency=2.45e9))
await asyncio.sleep(0.01)
await s.on_message(_tx_start(app_id="b", tx_gain=-20, tx_center_frequency=2.45e9))
# Let the second request process, then stop cleanly.
await asyncio.sleep(0.01)
await s.on_message({"type": "tx_stop", "app_id": "a"})
return ws
ws = asyncio.run(scenario())
errors = [m for m in ws.json_sent if m.get("type") == "tx_status" and m.get("state") == "error"]
assert any("already active" in e.get("message", "") for e in errors)
def test_rejects_invalid_underrun_policy():
async def scenario():
s, ws, _ = _make_streamer(AgentConfig(tx_enabled=True))
await s.on_message(
{
"type": "tx_start",
"app_id": "a",
"radio_config": {
"device": "mock",
"buffer_size": 8,
"tx_gain": -20,
"tx_center_frequency": 2.45e9,
"underrun_policy": "teleport",
},
}
)
return ws
ws = asyncio.run(scenario())
status = _last_tx_status(ws)
assert status and status["state"] == "error"
assert "underrun_policy" in status["message"]