feat(subscription): store-native slice 6 and remove OPTIONS 26s loop

Drop runtime router→store overwrites; refresh static TGs via RPTO,
startup/reload apply_startup_bridges, and dmrd fallback only (V2-P2-016).
pull/1/head
Rodrigo Pérez 4 months ago
parent 21d12835b7
commit 2ec8ff1d5e

@ -6,6 +6,7 @@ All notable changes to **adn-server** are documented here. Versioning follows [S
### Added
- SIGHUP reload reapplies YAML static TGs via `apply_startup_bridges()` when SYSTEMS change.
- Store-native timer loops (`rule_timer`, `stat_trimmer`, `bridge_debug`, `bridge_reset`) and in-band signalling.
- Store-native bridge table (OPTIONS, static TG, reflectors, UA) and OBP source ensure.
- `InbandTriggers` (ON/OFF/RESET) on `Subscription` with import/export round-trip.
@ -22,6 +23,11 @@ All notable changes to **adn-server** are documented here. Versioning follows [S
- Dashboard peers without RPTO inherit `TS1_STATIC` / `TS2_STATIC` from the MASTER YAML.
- Inject-only monitor remap: sole connected hotspot receives dynamic UA downlink when a bridge leg is ACTIVE.
### Removed
- Periodic 26s `options_config_loop` (V2-P2-016): OPTIONS/static TGs covered by RPTO, startup/reload, and dmrd fallback.
- Runtime `replace_store_from_bridges` calls in bridge timers and OPTIONS paths (store-native slice 6).
## [2.0.0-alpha.2] - 2026-06-11
Subscription runtime (phase 2b/2c) closed for production Chile.

@ -19,7 +19,7 @@ The following intervals are part of the current runtime behavior:
| `rule_timer` | **52s** | Bridge timeout/on-off state progression. |
| `stream_trimmer` | **5s** | Stream cleanup, timeout handling, end-of-call state trimming. |
| `bridge_reset` | **6s** | Bridge reset flag cleanup and pending reset completion. |
| `options_config_loop` | **26s** | Refresh static TG / reflector options from peer OPTIONS payloads. |
| OPTIONS refresh | **event-driven** | Static TG / reflector from **RPTO**, **startup/reload** (`apply_startup_bridges`), **dmrd** no-source fallback. No periodic 26s loop (**D-28**). |
| `statTrimmer` | **303s** | Trim stale STAT bridges and transient status entries. |
If you change one of these intervals, document the operational impact for monitoring, loop behavior, and troubleshooting.

@ -19,7 +19,7 @@ Los siguientes intervalos forman parte del comportamiento actual en ejecución:
| `rule_timer` | **52s** | Progresión de timeout y estado on/off de bridges. |
| `stream_trimmer` | **5s** | Limpieza de streams, manejo de timeout y cierre de estado de llamada. |
| `bridge_reset` | **6s** | Limpieza de flags de reset y cierre de resets pendientes. |
| `options_config_loop` | **26s** | Refresco de TG estáticas / reflector desde payload OPTIONS de peers. |
| OPTIONS refresh | **por evento** | TG estáticas / reflector vía **RPTO**, **startup/reload** (`apply_startup_bridges`), fallback **dmrd** sin source. Sin loop periódico de 26s (**D-28**). |
| `statTrimmer` | **303s** | Limpieza de bridges STAT obsoletos y estados transitorios. |
Si cambias uno de estos intervalos, documenta el impacto operativo en monitorización, comportamiento de bucles y troubleshooting.

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
class BridgeTableMixin:
"""make_single_bridge, stat/static TG, options_config_loop."""
"""make_single_bridge, stat/static TG, OPTIONS refresh (RPTO / startup / dmrd)."""
def make_single_bridge(
self,
@ -32,9 +32,7 @@ class BridgeTableMixin:
if _tgid_s in ("9990", "9991", "9992", "9993", "9994", "9995", "9996", "9997", "9998", "9999"):
_tmout = 1.0 / 6.0
from ..subscription.bridge_table_ops import make_single_bridge_store
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
make_single_bridge_store(
self._subscription_store,
tgid_int,
@ -131,9 +129,7 @@ class BridgeTableMixin:
"""Legacy make_stat_bridge: on-the-fly relay bridges for OBP traffic when GEN_STAT_BRIDGES is True."""
_tgid_s = str(int_id(_tgid))
from ..subscription.bridge_table_ops import make_stat_bridge_store
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
make_stat_bridge_store(
self._subscription_store,
_tgid,
@ -145,9 +141,7 @@ class BridgeTableMixin:
def deactivate_all_dynamic_bridges(self, system_name: str) -> None:
"""Legacy deactivate_all_dynamic_bridges: deactivate all non-STAT, non-reflector bridges for a system (TG 4000)."""
from ..subscription.bridge_table_ops import deactivate_all_dynamic_bridges_store
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
deactivate_all_dynamic_bridges_store(self._subscription_store, system_name)
self._export_store_to_router()
@ -164,9 +158,6 @@ class BridgeTableMixin:
def apply_startup_bridges(self) -> None:
"""Legacy startup: set default reflectors and static TGs for each MASTER system."""
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
prohibited_tgs = (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999)
logger.debug("(ROUTER) Setting default reflectors")
for system, sys_cfg in self._config.get("SYSTEMS", {}).items():
@ -200,8 +191,78 @@ class BridgeTableMixin:
if tg in prohibited_tgs:
continue
self.make_static_tg(tg, 2, tmout, system)
for system, sys_cfg in self._config.get("SYSTEMS", {}).items():
if sys_cfg.get("MODE") != "MASTER":
continue
if not sys_cfg.get("ENABLED", True):
continue
self.options_config_for_system(system)
self._sync_subscription_store()
def _first_connected_peer_options(self, system_name: str) -> bytes | str | None:
"""First connected peer OPTIONS (legacy options_config peer scan without 26s loop)."""
protocols = self._get_protocols() if self._get_protocols else {}
proto = protocols.get(system_name)
peers = getattr(proto, "_peers", {}) if proto is not None else {}
if not isinstance(peers, dict):
return None
for peer in peers.values():
if isinstance(peer, dict) and peer.get("CONNECTION") == "YES" and peer.get("OPTIONS"):
return peer["OPTIONS"]
return None
def _options_key_allows(self, system_name: str, parsed: dict[str, Any]) -> bool:
"""Legacy OPTIONS KEY gate (_opt_key on MASTER)."""
sys_cfg = self._config.get("SYSTEMS", {}).get(system_name, {})
if sys_cfg.get("_opt_key"):
if "KEY" not in parsed:
logger.debug(
"(OPTIONS) %s, options key set but no key in options string, skipping",
system_name,
)
return False
if sys_cfg["_opt_key"] != parsed.get("KEY"):
logger.debug(
"(OPTIONS) %s, options key set but key sent does not match, skipping",
system_name,
)
return False
elif parsed.get("KEY"):
sys_cfg["_opt_key"] = parsed["KEY"]
logger.debug(
"(OPTIONS) %s, _opt_key not set but key sent. Setting to sent key",
system_name,
)
else:
sys_cfg["_opt_key"] = False
return True
def _maybe_update_reflector_from_options(
self, system_name: str, parsed: dict[str, Any]
) -> None:
"""Apply DEFAULT_REFLECTOR / DIAL changes from parsed OPTIONS (legacy options_config)."""
prohibited_tgs = (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999)
sys_cfg = self._config.get("SYSTEMS", {}).get(system_name, {})
raw_timer = parsed.get("DEFAULT_UA_TIMER", sys_cfg.get("DEFAULT_UA_TIMER", 10))
try:
timer_int = int(raw_timer)
tmout = float(35791394 if timer_int == 0 else timer_int)
except (TypeError, ValueError):
tmout = float(sys_cfg.get("DEFAULT_UA_TIMER", 10))
new_ref = int(parsed.get("DEFAULT_REFLECTOR", 0) or 0)
cur_ref = int(sys_cfg.get("DEFAULT_REFLECTOR", 0) or 0)
if new_ref == cur_ref:
return
if new_ref > 0:
logger.debug("(OPTIONS) %s default reflector changed, updating", system_name)
self.reset_all_reflector_system(tmout, system_name)
self.make_default_reflector(new_ref, tmout, system_name)
elif new_ref in prohibited_tgs and not bool(new_ref):
logger.debug("(OPTIONS) %s default reflector is prohibited, ignoring change", system_name)
else:
logger.debug("(OPTIONS) %s default reflector disabled, updating", system_name)
self.reset_all_reflector_system(tmout, system_name)
def _parse_options_string(self, opt_str: bytes | str) -> dict[str, Any] | None:
"""Parse hotspot OPTIONS / RPTO payload into a normalized options dict."""
try:
@ -401,24 +462,22 @@ class BridgeTableMixin:
``peer_options`` from RPTO overrides YAML (inject-only proxy: OPTIONS live on each peer).
"""
if not getattr(self, "_options_store_batch", False):
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
prohibited_tgs = (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999)
systems_cfg = self._config.get("SYSTEMS", {})
sys_cfg = systems_cfg.get(system_name, {})
if sys_cfg.get("MODE") != "MASTER":
return
try:
if peer_options is not None:
parsed_peer = self._parse_options_string(peer_options)
if parsed_peer:
self._apply_master_runtime_options(system_name, parsed_peer)
elif "OPTIONS" in sys_cfg:
parsed_sys = self._parse_options_string(sys_cfg["OPTIONS"])
if parsed_sys:
self._apply_master_runtime_options(system_name, parsed_sys)
runtime_source: bytes | str | None = peer_options
if runtime_source is None and "OPTIONS" in sys_cfg:
runtime_source = sys_cfg["OPTIONS"]
if runtime_source is None:
runtime_source = self._first_connected_peer_options(system_name)
if runtime_source is not None:
parsed_runtime = self._parse_options_string(runtime_source)
if parsed_runtime and self._options_key_allows(system_name, parsed_runtime):
self._apply_master_runtime_options(system_name, parsed_runtime)
self._maybe_update_reflector_from_options(system_name, parsed_runtime)
source_opt: bytes | str | None = peer_options
if source_opt is None and "OPTIONS" in sys_cfg:
@ -715,95 +774,3 @@ class BridgeTableMixin:
if len(lines) > 1:
logger.debug("\n".join(lines))
def options_config_loop(self) -> None:
"""Legacy options_config: parse OPTIONS from MASTER systems and update bridges (default reflector, static TGs)."""
batch_store = True
if batch_store:
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
self._options_store_batch = True
prohibited_tgs = (0, 1, 2, 3, 4, 5, 9, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999)
logger.debug("(OPTIONS) Running options parser")
systems_cfg = self._config.get("SYSTEMS", {})
try:
for _system in list(systems_cfg.keys()):
try:
if systems_cfg.get(_system, {}).get("MODE") != "MASTER":
continue
if not systems_cfg.get(_system, {}).get("ENABLED", True):
continue
opt_str: bytes | str | None = systems_cfg.get(_system, {}).get("OPTIONS")
if opt_str is None:
protocols = self._get_protocols() if self._get_protocols else {}
proto = protocols.get(_system)
peers = getattr(proto, "_peers", {}) if proto is not None else {}
if isinstance(peers, dict):
for peer in peers.values():
if isinstance(peer, dict) and peer.get("CONNECTION") == "YES" and peer.get("OPTIONS"):
opt_str = peer["OPTIONS"]
if opt_str is None:
continue
_options = self._parse_options_string(opt_str)
if not _options:
continue
logger.debug("(OPTIONS) Options found for %s", _system)
if "_opt_key" in systems_cfg[_system] and systems_cfg[_system].get("_opt_key"):
if "KEY" not in _options:
logger.debug("(OPTIONS) %s, options key set but no key in options string, skipping", _system)
continue
if systems_cfg[_system]["_opt_key"] != _options.get("KEY"):
logger.debug("(OPTIONS) %s, options key set but key sent does not match, skipping", _system)
continue
elif _options.get("KEY"):
systems_cfg[_system]["_opt_key"] = _options["KEY"]
logger.debug("(OPTIONS) %s, _opt_key not set but key sent. Setting to sent key", _system)
else:
systems_cfg[_system]["_opt_key"] = False
logger.debug("(OPTIONS) %s, _opt_key not set and no key sent. Set to false", _system)
self._apply_master_runtime_options(_system, _options)
_options.setdefault("TS1_STATIC", False)
_options.setdefault("TS2_STATIC", False)
_options.setdefault("DEFAULT_REFLECTOR", 0)
_options.setdefault("OVERRIDE_IDENT_TG", False)
_options.setdefault("DEFAULT_UA_TIMER", systems_cfg[_system].get("DEFAULT_UA_TIMER", 10))
if "TS1_STATIC" not in _options or "TS2_STATIC" not in _options or "DEFAULT_REFLECTOR" not in _options or "DEFAULT_UA_TIMER" not in _options:
logger.debug("(OPTIONS) %s - Required field missing, ignoring", _system)
continue
if _options["TS1_STATIC"] == "":
_options["TS1_STATIC"] = False
if _options["TS2_STATIC"] == "":
_options["TS2_STATIC"] = False
if _options.get("TS1_STATIC") and re.search(r"[^\d,]", str(_options["TS1_STATIC"])):
logger.debug("(OPTIONS) %s - TS1_STATIC contains characters other than numbers and comma, ignoring", _system)
continue
if _options.get("TS2_STATIC") and re.search(r"[^\d,]", str(_options["TS2_STATIC"])):
logger.debug("(OPTIONS) %s - TS2_STATIC contains characters other than numbers and comma, ignoring", _system)
continue
for key in ("DEFAULT_REFLECTOR", "OVERRIDE_IDENT_TG", "DEFAULT_UA_TIMER"):
if isinstance(_options.get(key), str) and not str(_options[key]).isdigit():
logger.debug("(OPTIONS) %s - %s is not an integer, ignoring", _system, key)
continue
if int(_options.get("DEFAULT_UA_TIMER", 0)) == 0:
_options["DEFAULT_UA_TIMER"] = 35791394
_tmout = float(int(_options["DEFAULT_UA_TIMER"]))
new_ref = int(_options.get("DEFAULT_REFLECTOR", 0))
cur_ref = int(systems_cfg[_system].get("DEFAULT_REFLECTOR", 0))
if new_ref != cur_ref:
if new_ref > 0:
logger.debug("(OPTIONS) %s default reflector changed, updating", _system)
self.reset_all_reflector_system(_tmout, _system)
self.make_default_reflector(new_ref, _tmout, _system)
elif new_ref in prohibited_tgs and not bool(new_ref):
logger.debug("(OPTIONS) %s default reflector is prohibited, ignoring change", _system)
else:
logger.debug("(OPTIONS) %s default reflector disabled, updating", _system)
self.reset_all_reflector_system(_tmout, _system)
self.options_config_for_system(_system)
except Exception as e:
logger.exception("(OPTIONS) caught exception: %s", e)
finally:
if batch_store:
self._options_store_batch = False
self._sync_subscription_store()

@ -20,8 +20,7 @@ class BridgeTimerMixin:
def rule_timer_loop(self) -> None:
"""Run one iteration of rule_timer_loop (legacy 52s LoopingCall). Activate/deactivate by timeout."""
from ..subscription.rule_timer_ops import apply_rule_timer_store
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
apply_rule_timer_store(
self._subscription_store,
self._config.get("SYSTEMS", {}),
@ -33,8 +32,7 @@ class BridgeTimerMixin:
"""Legacy bridgeDebug (bridge_master.py 487-543): remove invalid bridges, fix >1 active dial per MASTER."""
logger.debug("(BRIDGEDEBUG) Running bridge debug")
from ..subscription.bridge_debug_ops import apply_bridge_debug_store
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
apply_bridge_debug_store(
self._subscription_store,
self._config.get("SYSTEMS", {}),
@ -50,8 +48,7 @@ class BridgeTimerMixin:
De-activation distinguishes SINGLE_MODE True/False (legacy ~3484-3548).
"""
from ..subscription.in_band_signalling_ops import apply_in_band_signalling_store
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
apply_in_band_signalling_store(
self._subscription_store,
system_name,
@ -319,8 +316,7 @@ class BridgeTimerMixin:
deactivate_system_legs_store,
restore_prohibited_static_legs_store,
)
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
now = time.time()
for system_name in list(systems_cfg.keys()):
sys_cfg = systems_cfg.get(system_name, {})
@ -357,7 +353,6 @@ class BridgeTimerMixin:
"""Trim STAT-only bridges with no ON/OFF in use (legacy statTrimmer, 303s)."""
logger.debug("(ROUTER) STAT trimmer loop started")
from ..subscription.stat_trimmer_ops import apply_stat_trimmer_store
from ..subscription.store_sync import replace_store_from_bridges
replace_store_from_bridges(self._subscription_store, self._router.get_bridges())
apply_stat_trimmer_store(self._subscription_store)
self._export_store_to_router()

@ -1,4 +1,8 @@
"""Keep ``SubscriptionStore`` aligned with legacy ``BRIDGES`` (read-only mirror; not routing authority yet)."""
"""Import legacy ``BRIDGES`` snapshots into ``SubscriptionStore`` (tests and harness only).
Runtime paths mutate the store directly and publish via ``export_bridges``; they must not
call ``replace_store_from_bridges`` — that would overwrite store authority with the shim.
"""
from __future__ import annotations

@ -521,6 +521,7 @@ def run_peer_server(
return not is_proxy_inject_only(config, system_name)
def _on_config_systems_changed() -> None:
bridge_use_cases.apply_startup_bridges()
reporting_use_cases.send_config(config.get("SYSTEMS", {}))
reporting_use_cases.send_bridge(get_bridges())
user_passwords_loader.load(config)
@ -611,7 +612,6 @@ def run_peer_server(
reactor.callInThread(ident_use_cases.run_ident)
task.LoopingCall(ident_loop).start(3600).addErrback(_looping_errback, logger)
task.LoopingCall(bridge_use_cases.options_config_loop).start(26).addErrback(_looping_errback, logger)
task.LoopingCall(bridge_use_cases.log_connected_systems_and_tgs).start(60).addErrback(_looping_errback, logger)
task.LoopingCall(lambda: logger.debug("(ROUTER) KeepAlive reporting loop started")).start(60).addErrback(_looping_errback, logger)

@ -55,7 +55,7 @@ Mark new stack tests with `@pytest.mark.integration`.
| `test_config_reload.py` | 1 | Merge system config on reload |
| `test_crc_dedup.py` | 3 | HBP/OBP CRC dedup, seq=0 |
| `test_echo_bridgereset.py` | 5 | Echo leg after BRIDGERESET / OPTIONS |
| `test_options_config_loop.py` | 2 | 26s OPTIONS static TG |
| `test_options_config_loop.py` | 3 | OPTIONS paths (RPTO/startup; no 26s loop) |
| `test_private_voice.py` | 3 | Private call routing |
| `test_startup_bridges.py` | 4 | Startup BRIDGES + voice E2E |
| `test_static_tg_options.py` | 4 | Static TG from peer OPTIONS |

@ -36,3 +36,19 @@ def test_merge_system_config_preserves_peers_and_static_tgs() -> None:
assert merged["TS2_STATIC"] == old["TS2_STATIC"]
assert merged["_options_static_apply_fp"] == old["_options_static_apply_fp"]
assert merged["GROUP_HANGTIME"] == 3
def test_merge_new_master_keeps_yaml_static_tg() -> None:
"""New MASTER on reload keeps YAML static lists (apply_startup_bridges materializes them)."""
new = {
"MODE": "MASTER",
"ENABLED": True,
"IP": "127.0.0.1",
"PORT": 62032,
"TS2_STATIC": "52090",
"DEFAULT_UA_TIMER": 10,
}
merged = merge_system_config({}, new)
assert merged["TS2_STATIC"] == "52090"

@ -1,32 +1,47 @@
"""OPTIONS timer loop (options_config_loop, 26s refresh)."""
"""OPTIONS refresh paths (RPTO, startup, connected peer — no 26s loop, V2-P2-016)."""
from __future__ import annotations
from tests.harness.deterministic import DeterministicScenario
def test_options_config_loop_applies_static_tg_from_options() -> None:
def test_options_config_for_system_applies_static_tg_from_yaml_options() -> None:
config = DeterministicScenario().config
config["SYSTEMS"]["MASTER-A"]["OPTIONS"] = "TS2=52090;TIMER=10"
config["SYSTEMS"]["MASTER-A"]["TS1_STATIC"] = ""
config["SYSTEMS"]["MASTER-A"]["TS2_STATIC"] = ""
scenario = DeterministicScenario(config=config)
scenario.bridge.options_config_loop()
scenario.bridge.options_config_for_system("MASTER-A")
sys_cfg = scenario.config["SYSTEMS"]["MASTER-A"]
assert sys_cfg["TS2_STATIC"] == "52090"
assert "52090" in scenario.bridge.get_bridges()
def test_options_config_loop_skips_master_without_options() -> None:
def test_options_config_for_system_skips_master_without_options() -> None:
config = DeterministicScenario().config
config["SYSTEMS"]["MASTER-A"].pop("OPTIONS", None)
config["SYSTEMS"]["MASTER-B"]["OPTIONS"] = "TS2=91;TIMER=10"
config["SYSTEMS"]["MASTER-B"]["TS2_STATIC"] = ""
scenario = DeterministicScenario(config=config)
scenario.bridge.options_config_loop()
scenario.bridge.options_config_for_system("MASTER-A")
scenario.bridge.options_config_for_system("MASTER-B")
assert scenario.config["SYSTEMS"]["MASTER-A"].get("TS2_STATIC", "") == ""
assert scenario.config["SYSTEMS"]["MASTER-B"]["TS2_STATIC"] == "91"
def test_apply_startup_runs_options_config_for_each_master() -> None:
"""Startup/reload path: YAML OPTIONS without TS*_STATIC still materializes bridges."""
config = DeterministicScenario().config
config["SYSTEMS"]["MASTER-A"]["OPTIONS"] = "TS2=52090;TIMER=10"
config["SYSTEMS"]["MASTER-A"]["TS1_STATIC"] = ""
config["SYSTEMS"]["MASTER-A"]["TS2_STATIC"] = ""
scenario = DeterministicScenario(config=config)
scenario.bridge.apply_startup_bridges()
assert scenario.config["SYSTEMS"]["MASTER-A"]["TS2_STATIC"] == "52090"
assert "52090" in scenario.bridge.get_bridges()

@ -58,12 +58,12 @@ def test_rpto_single_and_timer_override_yaml() -> None:
assert scenario.bridge.get_bridges()["52090"][0]["TIMEOUT"] == 300.0
def test_options_loop_reads_connected_peer_without_yaml_options() -> None:
def test_options_config_reads_connected_peer_without_yaml_options() -> None:
scenario = _proxy_system_scenario(single_mode_yaml=False)
bridges = active_bridge(730444, (("MASTER-A", 2), ("MASTER-B", 2)))
scenario.router.set_bridges(bridges)
scenario.bridge.options_config_loop()
scenario.bridge.options_config_for_system("MASTER-A")
assert scenario.config["SYSTEMS"]["MASTER-A"]["SINGLE_MODE"] is True
@ -224,7 +224,7 @@ def test_peer_timer_does_not_override_other_peer_static_tg_timeout() -> None:
def test_make_static_tg_preserves_single_mode_deactivation_on_options_refresh() -> None:
"""OPTIONS loop (make_static_tg) must not re-activate TGs turned off by SINGLE VTERM."""
"""OPTIONS refresh (make_static_tg) must not re-activate TGs turned off by SINGLE VTERM."""
scenario = _proxy_system_scenario(single_mode_yaml=True)
bridges = {
"730444": [

Loading…
Cancel
Save

Powered by TurnKey Linux.