fix(cron): routed-profile cron delivers through the shared bot for guild-scoped routes and profiles without a platforms block
SharedRouteAdapters.get called ProfileRoute.matches without guild_id, so the
documented Discord route shape (guild_id + chat_id) never authorized a cron
target and the satellite fell to standalone delivery ("DISCORD_BOT_TOKEN is
not set" every fire). A cron target has no inbound guild anchor; the route's
own guild_id is passed so its target-exact discriminators decide.
_resolve_target_transport then vetoed the authorized shared transport on the
SATELLITE's platforms.<p>.enabled (absent block or enabled: false), although
that block describes a connector the satellite never runs. The shared hit now
builds the transport directly (keeping the satellite's non-credential platform
settings), and a live native adapter with no config block is no longer read as
"disabled" (#89302) — same normalization the relay path already had.
Fixes #89302
Co-authored-by: web3blind <264741654+web3blind@users.noreply.github.com>
This commit is contained in:
@@ -1133,15 +1133,28 @@ def _resolve_target_transport(
|
||||
"""Resolve ``(transport, pconfig, runtime_adapter, target_adapters)`` for one target, or
|
||||
``(None, error)`` when it cannot be served (relay-fronted with no live transport, or not
|
||||
configured/enabled)."""
|
||||
from gateway.delivery import resolve_delivery_transport
|
||||
from gateway.delivery import DeliveryTransport, resolve_delivery_transport
|
||||
target_adapters = adapters
|
||||
transport = None
|
||||
if isinstance(adapters, _preflight.SharedRouteAdapters):
|
||||
# Credentialless satellite: the primary adapter serves THIS target only when an exact
|
||||
# primary route maps it to this profile; a miss fails closed below.
|
||||
# See #101113.
|
||||
shared = adapters.get(platform, target)
|
||||
target_adapters = {platform: shared} if shared is not None else {}
|
||||
transport = resolve_delivery_transport(platform, config, target_adapters)
|
||||
if shared is not None:
|
||||
# The PRIMARY's route authorized this exact native adapter. The satellite's own
|
||||
# ``platforms.<p>`` block describes a connector it never runs (no credential), so
|
||||
# neither its absence nor ``enabled: false`` may veto the shared transport; only its
|
||||
# non-credential settings (continuable surface, reply mode) are kept (#89302, #103701).
|
||||
from dataclasses import replace
|
||||
from gateway.config import PlatformConfig
|
||||
own = config.platforms.get(platform)
|
||||
transport = DeliveryTransport(
|
||||
shared, replace(own, enabled=True) if own is not None else PlatformConfig(enabled=True),
|
||||
platform)
|
||||
if transport is None:
|
||||
transport = resolve_delivery_transport(platform, config, target_adapters)
|
||||
if transport is not None:
|
||||
pconfig = transport.config
|
||||
runtime_adapter = transport.adapter
|
||||
@@ -1159,9 +1172,11 @@ def _resolve_target_transport(
|
||||
pconfig = config.platforms.get(platform)
|
||||
runtime_adapter = None
|
||||
|
||||
if transport is not None and transport.is_relay:
|
||||
# Relay transport carries the RELAY adapter's config (enablement already checked). The
|
||||
# logical platform is deliberately NOT natively enabled, so the native gate must not apply.
|
||||
if transport is not None and (transport.is_relay or pconfig is None):
|
||||
# Relay transport carries the RELAY adapter's config (enablement already checked): the
|
||||
# logical platform is deliberately NOT natively enabled. A live NATIVE adapter with no
|
||||
# ``platforms.<p>`` block is the same shape — the owning process already authorized the
|
||||
# adapter; "no config" is not "disabled" (#89302).
|
||||
if pconfig is None:
|
||||
from gateway.config import PlatformConfig
|
||||
pconfig = PlatformConfig(enabled=True)
|
||||
|
||||
@@ -198,12 +198,17 @@ class SharedRouteAdapters:
|
||||
chat_id = str(target.get("chat_id") or "") or None
|
||||
thread_id = target.get("thread_id")
|
||||
thread_id = str(thread_id) if thread_id else None
|
||||
# A cron target carries no inbound guild anchor, so a route's guild_id is matched against
|
||||
# itself — the target-exact discriminators (chat_id/thread_id) authorize the send. Without
|
||||
# this the documented ``guild_id + chat_id`` Discord route never authorized cron output.
|
||||
for route in self._routes:
|
||||
if str(route.platform).lower() != platform_key:
|
||||
continue
|
||||
if not (route.chat_id or route.thread_id):
|
||||
continue # guild-only routes are not target-exact
|
||||
if route.matches(str(route.platform), chat_id=chat_id, thread_id=thread_id):
|
||||
if route.matches(
|
||||
str(route.platform), guild_id=route.guild_id, chat_id=chat_id, thread_id=thread_id,
|
||||
):
|
||||
return adapter
|
||||
return default
|
||||
|
||||
|
||||
@@ -107,3 +107,56 @@ def test_satellite_routes_exact_target_through_primary_adapter(tmp_path, monkeyp
|
||||
def test_shared_view_is_falsy_without_routes_or_primary_adapters():
|
||||
assert not SharedRouteAdapters({}, [])
|
||||
assert SharedRouteAdapters({Platform.DISCORD: object()}, []).get(Platform.DISCORD) is None
|
||||
|
||||
|
||||
def test_guild_scoped_route_authorizes_cron_target_even_when_satellite_has_no_platform_block(
|
||||
tmp_path, monkeypatch,
|
||||
):
|
||||
"""The documented Discord route shape is ``guild_id + chat_id``. A cron target carries no guild
|
||||
anchor, so the route must be matched on its target-exact discriminators; and the satellite's
|
||||
missing/disabled ``platforms.discord`` block must not veto the PRIMARY's authorized transport
|
||||
(#89302 sibling) — before, both fell to standalone "DISCORD_BOT_TOKEN is not set"."""
|
||||
root = tmp_path / "root"
|
||||
sat_home = root / "profiles" / "fitness"
|
||||
sat_home.mkdir(parents=True)
|
||||
(root / "config.yaml").write_text(yaml.safe_dump({
|
||||
"gateway": {"multiplex_profiles": True, "profile_routes": [
|
||||
{"platform": "discord", "guild_id": "G1", "chat_id": "C1", "profile": "fitness"}]},
|
||||
}), encoding="utf-8")
|
||||
monkeypatch.setattr("hermes_constants.get_default_hermes_root", lambda: root)
|
||||
primary = _primary_adapter()
|
||||
|
||||
token = set_hermes_home_override(str(sat_home))
|
||||
try:
|
||||
shared = SharedRouteAdapters({Platform.DISCORD: primary}, _primary_profile_routes_for_current_home())
|
||||
for satellite_platforms in ({}, {Platform.DISCORD: PlatformConfig(enabled=False)}):
|
||||
primary.sent.clear()
|
||||
config = MagicMock()
|
||||
config.platforms = satellite_platforms
|
||||
config.get_home_channel = lambda p: None
|
||||
with patch("gateway.config.load_gateway_config", return_value=config):
|
||||
error, standalone = _run(_job("C1"), shared)
|
||||
assert error is None, error
|
||||
assert primary.sent == ["C1"] and standalone == []
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
|
||||
|
||||
def test_live_native_adapter_without_platform_block_is_not_treated_as_disabled():
|
||||
"""#89302: a live native adapter handed in by the gateway is the authorization; an absent
|
||||
``platforms.<p>`` block in the firing profile means "no config", not "disabled"."""
|
||||
from cron.scheduler_delivery import _resolve_target_transport
|
||||
|
||||
config = MagicMock()
|
||||
config.platforms = {}
|
||||
adapter = object()
|
||||
resolved, err = _resolve_target_transport(
|
||||
{"id": "j"}, Platform.DISCORD, "discord", {"platform": "discord", "chat_id": "C1"},
|
||||
{Platform.DISCORD: adapter}, config)
|
||||
assert err is None and resolved[2] is adapter and resolved[1].enabled
|
||||
# an explicitly disabled block still vetoes
|
||||
config.platforms = {Platform.DISCORD: PlatformConfig(enabled=False)}
|
||||
resolved, err = _resolve_target_transport(
|
||||
{"id": "j"}, Platform.DISCORD, "discord", {"platform": "discord", "chat_id": "C1"},
|
||||
{Platform.DISCORD: adapter}, config)
|
||||
assert resolved is None and "not configured/enabled" in err
|
||||
|
||||
Reference in New Issue
Block a user