fix(relay): treat the "relay" placeholder as an unresolved ack lane

Review caught the fallback never firing. `_event_from_wire` maps an
absent OR unknown wire platform to `Platform.RELAY`, so an unresolved
lane arrives as the truthy string "relay", never as "". Testing only for
"" made the chat-lane and descriptor fallbacks dead code: a
Telegram-primary gateway whose connector did not stamp the platform
still got ✅ and still failed.

Check each candidate in turn — event platform, the chat's inbound lane,
then the descriptor's primary — skipping "" and "relay", since any of
them can hold the placeholder. Reading the lane straight from
`_platform_by_chat` rather than `_chat_platform` matters too: that
helper folds in the descriptor itself, so a cached "relay" would shadow
a real primary platform.

The replaced test asserted on `platform=None`, a state the wire cannot
produce, which is why it passed while the path was broken. The new ones
build events through `_event_from_wire` for absent, explicit-"relay" and
unknown platforms, cover chat-lane precedence over a different primary,
and keep ✅ for a genuinely relay-primary gateway.
This commit is contained in:
Ben Barclay
2026-09-22 16:22:09 +10:00
committed by kshitij
parent ab24fb0581
commit f16a54bf7c
2 changed files with 63 additions and 18 deletions

View File

@@ -2252,25 +2252,35 @@ class RelayAdapter(BasePlatformAdapter):
_ACK_EMOJI_BY_PLATFORM = {
"telegram": ("👀", "👍", "👎"),
}
# `_event_from_wire` maps an absent OR unknown wire platform to
# `Platform.RELAY`, so an unresolved lane arrives as the truthy string
# "relay", never as "". Treating only "" as unresolved makes the fallback
# dead code and silently serves ✅ to a Telegram-primary gateway whose
# connector did not stamp the platform.
_ACK_PLATFORM_UNRESOLVED = frozenset({"", "relay"})
def _ack_emoji(self, event, chat_id) -> tuple:
"""(in_progress, success, failure) for the lane this event arrived on.
Prefers the EVENT's own platform: an ack always follows an inbound
event, so the platform is on hand and needs no cache. `_chat_platform`
is the fallback, and it can only answer for a chat already seen inbound
— the descriptor's primary platform covers the rest.
event, so the platform is on hand and needs no cache. Falls back to the
chat's lane as seen inbound, then to the descriptor's primary platform.
Each candidate is checked in turn because any of them can be the
placeholder "relay", which resolves nothing.
`Platform` is a plain `Enum`, so `str()` on a member yields
"Platform.TELEGRAM", not "telegram" — read `.value` first or every
lookup misses and silently falls back to the default set.
"""
raw = getattr(getattr(event, "source", None), "platform", None)
platform = str(getattr(raw, "value", raw) or "").lower()
if not platform:
lane = self._chat_platform(str(chat_id))
platform = str(getattr(lane, "value", lane) or "").lower()
return self._ACK_EMOJI_BY_PLATFORM.get(platform, self._ACK_EMOJI_DEFAULT)
for candidate in (
getattr(getattr(event, "source", None), "platform", None),
self._platform_by_chat.get(str(chat_id)),
getattr(self.descriptor, "platform", None),
):
name = str(getattr(candidate, "value", candidate) or "").lower()
if name and name not in self._ACK_PLATFORM_UNRESOLVED:
return self._ACK_EMOJI_BY_PLATFORM.get(name, self._ACK_EMOJI_DEFAULT)
return self._ACK_EMOJI_DEFAULT
async def _react(
self,

View File

@@ -29,6 +29,7 @@ from gateway.config import Platform, PlatformConfig
from gateway.platforms.event import MessageEvent, MessageType, ProcessingOutcome
from gateway.relay.adapter import RelayAdapter
from gateway.relay.descriptor import CONTRACT_VERSION, CapabilityDescriptor
from gateway.relay.ws_transport import _event_from_wire
from gateway.session import SessionSource
from tests.gateway.relay.stub_connector import StubConnector
@@ -315,16 +316,50 @@ async def test_free_form_reaction_platforms_keep_the_check_mark():
@pytest.mark.asyncio
async def test_ack_platform_falls_back_to_the_chat_lane_then_the_descriptor():
"""An event with no platform on its source still resolves: a multi-platform
gateway records the chat's lane inbound, and the descriptor's primary
platform covers a chat never seen inbound. A telegram-primary descriptor
must therefore never emit ✅."""
adapter, stub = _adapter() # make_desc default platform is telegram
event = _reactable_event(platform=None)
async def test_ack_platform_falls_back_through_the_real_wire_decode():
"""`_event_from_wire` maps an absent OR unknown wire platform to
`Platform.RELAY` — never to "" — so the fallback must treat the "relay"
placeholder as unresolved. Built through the real decoder, because an
event with no platform at all is a state the wire cannot produce."""
for wire_platform in ({}, {"platform": "relay"}, {"platform": "not_a_platform"}):
adapter, stub = _adapter() # make_desc default platform is telegram
event = _event_from_wire(
{
"text": "hi",
"message_id": "m42",
"source": {"chat_id": "ch1", "chat_type": "channel", "user_id": "u1", **wire_platform},
}
)
assert event.source.platform is Platform.RELAY, wire_platform
await adapter.on_processing_complete(event, ProcessingOutcome.SUCCESS)
emojis = [a["emoji"] for a in stub.sent if a["op"] == "react"]
assert emojis == ["👀", "👍"], f"{wire_platform} fell back to {emojis}"
@pytest.mark.asyncio
async def test_ack_platform_prefers_the_chat_lane_over_the_primary():
"""A multi-platform gateway records each chat's lane inbound. When the
event itself is unresolved, that lane must win over the primary platform —
otherwise every non-primary lane gets the primary's emoji."""
adapter, stub = _adapter(platform="slack", label="Slack")
adapter._platform_by_chat["ch1"] = "telegram"
event = _event_from_wire(
{"text": "hi", "message_id": "m42", "source": {"chat_id": "ch1", "chat_type": "channel"}}
)
await adapter.on_processing_complete(event, ProcessingOutcome.SUCCESS)
emojis = [a["emoji"] for a in stub.sent if a["op"] == "react"]
assert emojis == ["👀", "👍"]
assert [a["emoji"] for a in stub.sent if a["op"] == "react"] == ["👀", "👍"]
@pytest.mark.asyncio
async def test_ack_on_a_genuinely_generic_relay_keeps_the_check_mark():
"""Nothing resolves to a real platform: a relay-primary descriptor with no
inbound lane must not invent Telegram's vocabulary."""
adapter, stub = _adapter(platform="relay", label="Relay")
event = _event_from_wire(
{"text": "hi", "message_id": "m42", "source": {"chat_id": "ch1", "chat_type": "channel"}}
)
await adapter.on_processing_complete(event, ProcessingOutcome.SUCCESS)
assert [a["emoji"] for a in stub.sent if a["op"] == "react"] == ["👀", "✅"]
# ── fanned-out prompt answers (one press, many gateways) ─────────────────