Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""Cron delivery for relay-fronted logical platforms.
|
||||
|
||||
Bug report: a deployment where Discord is fronted by the relay connector
|
||||
(``GATEWAY_RELAY_PLATFORMS=discord``) could not use ``deliver='discord'``:
|
||||
|
||||
1. Resolution read ONLY the legacy ``DISCORD_HOME_CHANNEL`` env mirror, never
|
||||
the canonical ``platforms.discord.home_channel`` block that ``/sethome``
|
||||
persists to config.yaml — so the target silently resolved to nothing and
|
||||
the job fell back to local-only.
|
||||
2. Even with a resolved target, the delivery loop's native
|
||||
``pconfig.enabled`` gate rejected the platform ("not configured/enabled")
|
||||
although ``resolve_delivery_transport`` had already produced a live relay
|
||||
transport that fronts it — a relay-fronted logical platform is
|
||||
deliberately NOT natively enabled (its credential lives in the connector).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import Future
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cron import scheduler as sched
|
||||
from cron.scheduler import (
|
||||
_deliver_result,
|
||||
_get_home_target_chat_id,
|
||||
_get_home_target_thread_id,
|
||||
_resolve_delivery_targets,
|
||||
)
|
||||
from gateway.config import HomeChannel, Platform
|
||||
|
||||
|
||||
def _gateway_config_with_home(platform=Platform.DISCORD, chat_id="1517373704248758474",
|
||||
thread_id=None):
|
||||
"""A gateway config whose ONLY home-channel source is config.yaml."""
|
||||
home = HomeChannel(platform=platform, chat_id=chat_id, name="Home",
|
||||
thread_id=thread_id)
|
||||
config = MagicMock()
|
||||
config.platforms = {}
|
||||
config.get_home_channel = lambda p: home if p == platform else None
|
||||
return config
|
||||
|
||||
|
||||
def _clear_home_env(monkeypatch):
|
||||
for var in ("DISCORD_HOME_CHANNEL", "DISCORD_HOME_CHANNEL_THREAD_ID"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resolution: config.yaml home_channel fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestConfigHomeChannelFallback:
|
||||
def test_chat_id_falls_back_to_config_home_channel(self, monkeypatch):
|
||||
"""Env mirror empty → the canonical config.yaml home_channel is used."""
|
||||
_clear_home_env(monkeypatch)
|
||||
with patch("gateway.config.load_gateway_config",
|
||||
return_value=_gateway_config_with_home()):
|
||||
assert _get_home_target_chat_id("discord") == "1517373704248758474"
|
||||
|
||||
def test_env_mirror_still_wins_over_config(self, monkeypatch):
|
||||
"""Operator env override keeps precedence over the config block."""
|
||||
monkeypatch.setenv("DISCORD_HOME_CHANNEL", "999")
|
||||
with patch("gateway.config.load_gateway_config",
|
||||
return_value=_gateway_config_with_home()):
|
||||
assert _get_home_target_chat_id("discord") == "999"
|
||||
|
||||
def test_thread_id_falls_back_to_config_home_channel(self, monkeypatch):
|
||||
_clear_home_env(monkeypatch)
|
||||
with patch("gateway.config.load_gateway_config",
|
||||
return_value=_gateway_config_with_home(thread_id="777")):
|
||||
assert _get_home_target_thread_id("discord") == "777"
|
||||
|
||||
def test_config_thread_not_used_when_chat_came_from_env(self, monkeypatch):
|
||||
"""Thread affinity: a config thread_id must not be grafted onto an
|
||||
env-provided chat id (they may point at different conversations)."""
|
||||
monkeypatch.setenv("DISCORD_HOME_CHANNEL", "999")
|
||||
monkeypatch.delenv("DISCORD_HOME_CHANNEL_THREAD_ID", raising=False)
|
||||
with patch("gateway.config.load_gateway_config",
|
||||
return_value=_gateway_config_with_home(thread_id="777")):
|
||||
assert _get_home_target_thread_id("discord") is None
|
||||
|
||||
def test_no_source_returns_empty(self, monkeypatch):
|
||||
_clear_home_env(monkeypatch)
|
||||
config = MagicMock()
|
||||
config.platforms = {}
|
||||
config.get_home_channel = lambda p: None
|
||||
with patch("gateway.config.load_gateway_config", return_value=config):
|
||||
assert _get_home_target_chat_id("discord") == ""
|
||||
|
||||
def test_config_load_failure_fails_safe(self, monkeypatch):
|
||||
_clear_home_env(monkeypatch)
|
||||
with patch("gateway.config.load_gateway_config",
|
||||
side_effect=RuntimeError("boom")):
|
||||
assert _get_home_target_chat_id("discord") == ""
|
||||
|
||||
def test_deliver_discord_resolves_via_config_home(self, monkeypatch):
|
||||
"""End to end: deliver='discord' on a job with no origin resolves a
|
||||
concrete target from the config.yaml home_channel alone."""
|
||||
_clear_home_env(monkeypatch)
|
||||
job = {"id": "j1", "deliver": "discord"}
|
||||
with patch("gateway.config.load_gateway_config",
|
||||
return_value=_gateway_config_with_home()):
|
||||
targets = _resolve_delivery_targets(job)
|
||||
assert targets == [{
|
||||
"platform": "discord",
|
||||
"chat_id": "1517373704248758474",
|
||||
"thread_id": None,
|
||||
}]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Delivery: relay transport must bypass the native enabled gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRelayDeliveryGate:
|
||||
def _relay_adapter(self):
|
||||
adapter = AsyncMock()
|
||||
adapter.fronts_platform = lambda p: p == Platform.DISCORD
|
||||
return adapter
|
||||
|
||||
def _job(self):
|
||||
return {
|
||||
"id": "relay-job",
|
||||
"name": "Relay Job",
|
||||
"deliver": "discord",
|
||||
"origin": {"platform": "discord", "chat_id": "123"},
|
||||
}
|
||||
|
||||
def _run(self, adapters, gateway_config):
|
||||
loop = MagicMock()
|
||||
loop.is_running.return_value = True
|
||||
|
||||
def fake_run_coro(coro, _loop):
|
||||
future = Future()
|
||||
try:
|
||||
future.set_result(asyncio.run(coro))
|
||||
except BaseException as e: # noqa: BLE001
|
||||
future.set_exception(e)
|
||||
return future
|
||||
|
||||
router = MagicMock()
|
||||
|
||||
async def _deliver_to_platform(target, content, metadata):
|
||||
return {"success": True, "raw_response": None}
|
||||
|
||||
router._deliver_to_platform = _deliver_to_platform
|
||||
|
||||
with patch("gateway.config.load_gateway_config",
|
||||
return_value=gateway_config), \
|
||||
patch("cron.scheduler.load_config",
|
||||
return_value={"cron": {"wrap_response": False}}), \
|
||||
patch("gateway.delivery.DeliveryRouter", return_value=router), \
|
||||
patch("asyncio.run_coroutine_threadsafe", side_effect=fake_run_coro):
|
||||
return _deliver_result(self._job(), "Nightly report.",
|
||||
adapters=adapters, loop=loop)
|
||||
|
||||
def test_relay_fronted_platform_is_not_rejected(self, monkeypatch):
|
||||
"""A live relay transport that fronts Discord must deliver even though
|
||||
platforms.discord has no native config block at all."""
|
||||
_clear_home_env(monkeypatch)
|
||||
config = MagicMock()
|
||||
config.platforms = {} # neither discord nor relay configured natively
|
||||
config.get_home_channel = lambda p: None
|
||||
result = self._run({Platform.RELAY: self._relay_adapter()}, config)
|
||||
assert result is None # None == delivered without errors
|
||||
|
||||
def test_native_gate_preserved_without_relay(self, monkeypatch):
|
||||
"""No relay transport → the historical configured/enabled gate stays."""
|
||||
_clear_home_env(monkeypatch)
|
||||
config = MagicMock()
|
||||
config.platforms = {}
|
||||
config.get_home_channel = lambda p: None
|
||||
result = self._run({}, config)
|
||||
assert result is not None
|
||||
|
||||
def test_relay_fronted_without_live_gateway_errors_accurately(self, monkeypatch):
|
||||
"""A relay-fronted platform with NO live relay transport (manual
|
||||
in-process `hermes cron run`) fails with the accurate 'gateway required'
|
||||
message — never the native 'not configured/enabled' gate, which
|
||||
misdiagnoses relay-fronted deployments whose credential lives in the
|
||||
connector, not natively."""
|
||||
_clear_home_env(monkeypatch)
|
||||
monkeypatch.setenv("GATEWAY_RELAY_PLATFORMS", "discord")
|
||||
config = MagicMock()
|
||||
config.platforms = {}
|
||||
config.get_home_channel = lambda p: None
|
||||
result = self._run({}, config) # no live adapters
|
||||
assert result is not None
|
||||
assert "relay-fronted" in (result or "")
|
||||
assert "not configured" not in (result or "")
|
||||
Reference in New Issue
Block a user