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:
@@ -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,
|
||||
|
||||
@@ -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) ─────────────────
|
||||
|
||||
Reference in New Issue
Block a user